Typed messages and routing

A raw websocket is a pipe for bytes. To make it useful, you need a tiny message protocol on top. The convention is simple: every message is a JSON object with a type field and a payload. The server switches on the type and calls the right handler. Clients do the same on their end.

after/websocket/router.ts
typescript
import { WebSocket } from 'ws';
import { joinRoom, leaveRoom, broadcastToRoom } from './rooms';

type ClientMessage =
  | { type: 'subscribe'; channel: string }
  | { type: 'unsubscribe'; channel: string }
  | { type: 'ping' };

export function handleMessage(ws: WebSocket, raw: string) {
  let msg: ClientMessage;
  try {
    msg = JSON.parse(raw);
  } catch {
    ws.send(JSON.stringify({ type: 'error', message: 'invalid JSON' }));
    return;
  }

  switch (msg.type) {
    case 'subscribe':
      joinRoom(ws, msg.channel);
      ws.send(JSON.stringify({ type: 'subscribed', channel: msg.channel }));
      return;
    case 'unsubscribe':
      leaveRoom(ws, msg.channel);
      ws.send(JSON.stringify({ type: 'unsubscribed', channel: msg.channel }));
      return;
    case 'ping':
      ws.send(JSON.stringify({ type: 'pong' }));
      return;
  }
}

A discriminated union for messages and a router that dispatches them.

after/websocket/push.ts
typescript
import { broadcastToRoom } from './rooms';

export function announceBookAdded(book: { id: number; title: string }) {
  broadcastToRoom('catalog', {
    type: 'book_added',
    data: book,
  });
}

When the REST side creates a new book, it broadcasts a book_added event to everyone listening on the catalog channel.

Now read the full flow. A client subscribes to the catalog channel with a subscribe message. An admin creates a new book with a REST call. The controller calls announceBookAdded, which broadcasts a book_added message to every client in the catalog room. Every live catalog page updates instantly with no refresh.

Quiz: Quiz

Loading practiceโ€ฆ