3 ms·
After all my years of web development, my rules are thus: * If the browser has an optimal path for it, use HTTP (e.g. images where it caches them automatic
by riskable 11mo ago
After all my years of web development, my rules are thus:
* If the browser has an optimal path for it, use HTTP (e.g. images where it caches them automatically or file uploads where you get a "free" progress API).
* If I know my end users will be behind some shitty firewall that can't handle WebSockets (like we're still living in the early 2010s), use HTTP.
* Requests will be rare (per client): Use HTTP.
* For all else, use WebSockets.
WebSockets are just too awesome! You can use a simple event dispatcher for both the frontend and the backend to handle any given request/response and it makes the code sooooo much simpler than REST. Example:
WSDispatcher.on("pong", pongFunc);
...and `WSDispatcher` would be the (singleton) object that holds the WebSocket connection and has `on()`, `off()`, and `dispatch()` functions. When the server sends a message like `{"type": "pong", "payload": "<some timestamp>"}`, the client calls `WSDispatcher.dispatch("pong", "<some timestamp>")` which results in `pongFunc("<some timestamp>")` being called.
It makes reasoning about your API so simple and human-readable! It's also highly performant and fully async. With a bit of Promise wrapping, you can even make it behave like a synchronous call in your code which keeps the logic nice and concise.
In my latest pet project (collaborative editor) I've got the WebSocket API using a strict "call"/"call:ok" structure. Here's an example from my WEBSOCKET_API.md:
### Create Resource
```javascript
// Create story
send('resources:create', {
resource_type: 'story',
title: 'My New Story',
content: '',
tags: {},
policy: {}
});
// Create chapter (child of story)
send('resources:create', {
resource_type: 'chapter',
parent_id: 'story_abc123', // This would actually be a UUID
title: 'Chapter 1'
});
// Response:
{
type: 'resources:create:ok', // <- Note the ":ok"
resource: { id: '...', resource_type: '...', ... }
}
```
I've got a `request()` helper that makes the async nature of the WebSocket feel more like a synchronous call. Here's what that looks like in action:
const wsPromise = getWsService(); // Returns the WebSocket singleton
// Create resource (story, chapter, or file)
async function createResource(data: ResourcesCreateRequest) {
loading.value = true;
error.value = null;
try {
const ws = await wsPromise;
const response = await ws.request<ResourcesCreateResponse>(
"resources:create",
data // <- The payload
);
// resources.value because it's a Vue 3 `ref()`:
resources.value.push(response.resource);
return response.resource;
} catch (err: any) {
error.value = err?.message || "Failed to create resource";
throw err;
} finally {
loading.value = false;
}
}
For reference, errors are returned in a different, more verbose format where "type" is "error" in the object that the `request()` function knows how to deal with. It used to be ":err" instead of ":ok" but I made it different for a good reason I can't remember right now (LOL).
Aside: There's still THREE firewalls that suck so bad they can't handle WebSockets: SophosXG Firewall, WatchGuard, and McAfee Web Gateway.