WebSockets Cheatsheet
Sharing a Port with Express
Use this WebSockets reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
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.