WebSockets Cheatsheet
Sharing a Port with Express
Use this WebSockets reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Attach ws to an Express http.Server
import express from "express"; import { createServer } from "http"; import { WebSocketServer } from "ws"; const app = express(); const server = createServer(app); const wss = new WebSocketServer({ server }); // no `port` option // REST routes app.get("/health", (req, res) => res.json({ ok: true })); // WebSocket handler wss.on("connection", (ws, req) => { ws.send("connected"); ws.on("message", (data) => ws.send(data)); // echo }); server.listen(4000, () => console.log("HTTP + WS on :4000"));
Pass
server(notport) toWebSocketServer. Thewslibrary hooks into the HTTP server'supgradeevent.
Path-Based Routing (multiple WebSocket endpoints)
import { WebSocketServer } from "ws"; const wsChat = new WebSocketServer({ noServer: true }); const wsLive = new WebSocketServer({ noServer: true }); server.on("upgrade", (req, socket, head) => { const { pathname } = new URL(req.url, "http://x"); if (pathname === "/ws/chat") { wsChat.handleUpgrade(req, socket, head, (ws) => { wsChat.emit("connection", ws, req); }); } else if (pathname === "/ws/live") { wsLive.handleUpgrade(req, socket, head, (ws) => { wsLive.emit("connection", ws, req); }); } else { socket.destroy(); // reject unknown paths } }); wsChat.on("connection", (ws) => { /* chat logic */ }); wsLive.on("connection", (ws) => { /* live collab logic */ });
noServer Mode — Full Manual Control
// noServer: true — ws never binds a port or touches server.on("upgrade") const wss = new WebSocketServer({ noServer: true }); server.on("upgrade", (req, socket, head) => { // Optional: authenticate before upgrade const token = new URL(req.url, "http://x").searchParams.get("token"); if (!isValidToken(token)) { socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); socket.destroy(); return; } wss.handleUpgrade(req, socket, head, (ws) => { wss.emit("connection", ws, req); }); });
Express Middleware Before Upgrade
Express middleware does not run for WebSocket upgrade requests (they are not HTTP requests at the ws layer). Run auth inside the upgrade handler instead.
// Pattern: parse session cookie in upgrade handler import { parse as parseCookie } from "cookie"; import jwt from "jsonwebtoken"; server.on("upgrade", (req, socket, head) => { const cookies = parseCookie(req.headers.cookie ?? ""); const token = cookies["hu_session"]; let user; try { user = jwt.verify(token, process.env.JWT_SECRET); } catch { socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); socket.destroy(); return; } wss.handleUpgrade(req, socket, head, (ws) => { ws.user = user; wss.emit("connection", ws, req); }); });
Using express-ws
npm install express-ws
import expressWs from "express-ws"; import express from "express"; const app = express(); expressWs(app); // patches app in-place app.get("/api/status", (req, res) => res.json({ ok: true })); // WebSocket route — same router style as Express app.ws("/ws/chat", (ws, req) => { ws.on("message", (msg) => { ws.send(`echo: ${msg}`); }); }); app.listen(4000);
express-wswrapsnoServermode behind an Express-router interface. Useful for familiarity, butws+noServergives more control.
HTTPS / WSS Setup
import { createServer as createHttpsServer } from "https"; import { readFileSync } from "fs"; const httpsServer = createHttpsServer({ key: readFileSync("./certs/key.pem"), cert: readFileSync("./certs/cert.pem"), }); const wss = new WebSocketServer({ server: httpsServer }); app.use(/* ... */); httpsServer.on("request", app); httpsServer.listen(443);
In production, terminate TLS at a reverse proxy (nginx/Caddy/AWS ALB) and run Node on plain HTTP internally.
nginx Reverse Proxy Configuration
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
location /ws/ {
proxy_pass http://localhost:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s; # keep-alive for long-lived WS connections
proxy_send_timeout 3600s;
}
location / {
proxy_pass http://localhost:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Graceful Shutdown
process.on("SIGTERM", () => { console.log("Shutting down..."); // Close WebSocket server — no new connections wss.close(() => { // Close HTTP server server.close(() => { process.exit(0); }); }); // Notify and close existing clients for (const client of wss.clients) { client.close(1001, "Server shutting down"); } // Force-kill after timeout setTimeout(() => process.exit(1), 10_000); });
Port-Sharing Reference
| Approach | How | When to use |
|---|---|---|
{ server } option | Pass HTTP server to WebSocketServer | Single WS endpoint |
noServer + manual upgrade | Handle upgrade event yourself | Multiple endpoints, auth before upgrade |
express-ws | Router-style .ws() method | Express-centric codebases |
| Separate port | { port: 4001 } in WebSocketServer | Simple setups, separate TLS not needed |
Gotchas
- Do not use
app.use()for WebSocket auth — Express middleware only handles HTTP requests, not raw TCP upgrades. - The
upgradeevent fires for any protocol upgrade, including HTTP/2 — checkreq.headers.upgrade === "websocket"if you handle both. socket.destroy()in theupgradehandler must be called without writing anything, or the browser will show a confusing error; write a rejection response first if you want meaningful logging on the client.- With
{ server }mode and Express, theupgradeevent is consumed byws— you cannot also listen to it unless you usenoServer. proxy_read_timeoutin nginx defaults to 60 s — increase it for long-lived WebSocket connections or they will be silently dropped.