hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Real-Time APIs with @hectoday/http

Beyond Request-Response

  • Why Real-Time Matters
  • Project Setup

Polling

  • Short Polling
  • Long Polling

Server-Sent Events

  • How SSE Works
  • Building an SSE Endpoint
  • Event Types and IDs
  • SSE in Practice

WebSockets

  • How WebSockets Work
  • Building a WebSocket Server
  • Rooms and Broadcasting
  • Authentication on WebSockets
  • Handling Disconnects and Reconnection

Patterns and Architecture

  • Pub/Sub
  • Presence
  • Scaling Real-Time

Putting It All Together

  • Choosing the Right Approach
  • Capstone: Live Task Board

Building a WebSocket Server

WebSocket support in Node.js

Node.js does not include a WebSocket server natively. We use the ws library, which is the most widely used WebSocket implementation for Node.js.

npm install ws
npm install -D @types/ws

Setting up the WebSocket server

The WebSocket server shares the same HTTP server. The ws library upgrades incoming HTTP connections that request a WebSocket:

// src/ws-server.ts
import { WebSocketServer, WebSocket } from "ws";
import type { Server } from "node:http";

let wss: WebSocketServer;

export function setupWebSocketServer(server: Server): void {
  wss = new WebSocketServer({ server });

  wss.on("connection", (ws, request) => {
    console.log("WebSocket connected:", request.url);

    ws.on("message", (raw) => {
      try {
        const message = JSON.parse(raw.toString());
        handleMessage(ws, message);
      } catch {
        ws.send(JSON.stringify({ error: "Invalid JSON" }));
      }
    });

    ws.on("close", () => {
      console.log("WebSocket disconnected");
      handleDisconnect(ws);
    });

    ws.on("error", (err) => {
      console.error("WebSocket error:", err.message);
    });

    // Send welcome message
    ws.send(JSON.stringify({ type: "connected", message: "WebSocket connected" }));
  });
}

function handleMessage(ws: WebSocket, message: any): void {
  switch (message.type) {
    case "ping":
      ws.send(JSON.stringify({ type: "pong" }));
      break;
    default:
      ws.send(JSON.stringify({ error: `Unknown message type: ${message.type}` }));
  }
}

function handleDisconnect(ws: WebSocket): void {
  // Cleanup — implemented in later lessons
}

Integrating with the HTTP server

Update src/server.ts to pass the HTTP server to the WebSocket setup:

// src/server.ts
import { createServer } from "node:http";
import { app } from "./app.js";
import { setupWebSocketServer } from "./ws-server.js";

const server = createServer(async (req, res) => {
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
  const headers: Record<string, string> = {};
  for (const [key, value] of Object.entries(req.headers)) {
    if (typeof value === "string") headers[key] = value;
  }

  const request = new Request(url.toString(), {
    method: req.method,
    headers,
    body:
      req.method !== "GET" && req.method !== "HEAD"
        ? await new Promise<string>((resolve) => {
            let body = "";
            req.on("data", (chunk) => (body += chunk));
            req.on("end", () => resolve(body));
          })
        : undefined,
  });

  const response = await app.fetch(request);

  res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
  const text = await response.text();
  res.end(text);
});

setupWebSocketServer(server);
server.listen(3000, () => console.log("Server running on http://localhost:3000"));

[!NOTE] We switch from srvx to the raw http.createServer because the WebSocket upgrade requires access to the underlying HTTP server instance. The ws library attaches to this server and handles the upgrade automatically.

The message protocol

Define a simple JSON protocol for client-server communication:

// Client → Server messages:
{ "type": "ping" }
{ "type": "join", "boardId": "board-1" }
{ "type": "leave", "boardId": "board-1" }
{ "type": "typing", "boardId": "board-1" }

// Server → Client messages:
{ "type": "pong" }
{ "type": "connected", "message": "..." }
{ "type": "task_created", "task": { ... } }
{ "type": "user_joined", "userId": "...", "name": "..." }
{ "type": "user_typing", "userId": "...", "name": "..." }
{ "type": "error", "message": "..." }

Every message is a JSON object with a type field. The type determines the payload shape. This is a common pattern — Discord, Slack, and most WebSocket APIs use it.

Try it

# Start the server
npm run dev

# Connect with wscat (install globally: npm i -g wscat)
wscat -c ws://localhost:3000
# > Connected
# < {"type":"connected","message":"WebSocket connected"}

# Send a ping
> {"type":"ping"}
# < {"type":"pong"}

Exercises

Exercise 1: Set up the WebSocket server. Connect with wscat. Send a ping and verify you get a pong.

Exercise 2: Send an invalid JSON string (like hello). Verify you get an error response.

Exercise 3: Send a message with an unknown type (like {"type":"dance"}). Verify you get an error.

Why does the WebSocket server share the HTTP server instead of running on a separate port?

← How WebSockets Work Rooms and Broadcasting →

© 2026 hectoday. All rights reserved.