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
srvxto the rawhttp.createServerbecause the WebSocket upgrade requires access to the underlying HTTP server instance. Thewslibrary 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?