How can I define routes for Websockets using the ws library within an ExpressJS app? It's very easy to set it up the two layers in parallel, but the Websocket layer will not be able to benefit from ExpressJS middlewares (such as authentication). The only implementation I could find was express-ws, which is severely buggy due to not being up to date, and heavily relies on monkeypatching in order to work.
2 Answers
Partially modified from this answer. Modify your entry file to include this:
/* index.ts */
import http from 'http';
import express from 'express';
import exampleRouter from './exampleRouter';
// set up express and create a http server listen for websocket requests on the same port
const app = express();
const server = http.createServer(app);
// listen for websocket requests, which are simple HTTP GET requests with an upgrade header
// NOTE: this must occur BEFORE other middleware is set up if you want the additional ws.handled functionality to close unhandled requests
server.on('upgrade', (req: Request & { ws: { socket: Socket, head: Buffer, handled: Boolean } }, socket: Socket, head: Buffer) => {
// create a dummy response to pass the request into express
const res = new http.ServerResponse(req);
// assign socket and head to a new field in the request object
// optional **handled** field lets us know if there a route processed the websocket request, else we terminate it later on
req.ws = { socket, head, handled: false };
// have Express process the request
app(req, res);
});
/* whatever Express middlewares you want here, such as authentication */
app.use('/example', exampleRouter);
// set up a middleware to destroy unhandled websocket requests and returns a 403
// NOTE: this must occur AFTER your other middlewares but BEFORE the server starts listening for requests
app.use((req: Request & { ws?: { socket: Socket, head: Buffer, handled: Boolean } }, res: Response, next: NextFunction): void => {
if (req.ws && req.ws.handled === false) {
req.ws.socket.destroy();
res.status(404).json('404: Websocket route not found');
}
next();
});
const port = process.env.PORT || 8080;
server.listen(port);
Example of a Express Router with ws functionality, but the logic can be extracted to be used for one-offs
/* RouterWithWebSockets.ts */
// this is just a simple abstraction implementation so the you can set up multiple ws routes with the same router
// without having to rewrite the WSS code or monkeypatch the function into the Express Router directly
import express from 'express';
import { WebSocketServer, WebSocket } from 'ws';
class RouterWithWebSockets {
router;
constructor(router = express.Router()) {
this.router = router;
}
ws = (path: string, callback: (ws: WebSocket) => void, ...middleware: any): void => {
// set up a new WSS with the provided path/route to handle websockets
const wss = new WebSocketServer({
noServer: true,
path,
});
this.router.get(path, ...middleware, (req: any, res, next) => {
// just an extra check to deny upgrade requests if the path/route does not match
// you can process this route as a regular HTTP GET request if it's not a websocket upgrade request by replacing the next()
if (!req.headers.upgrade || path !== req.url) {
next();
} else {
req.ws.handled = true;
wss.handleUpgrade(req, req.ws.socket, req.ws.head, (ws: WebSocket) => {
callback(ws);
});
}
});
};
}
export default RouterWithWebSockets;
Finally, here is an example router with the Websocket routes
/* exampleRouter.ts */
const routerWithWebSockets = new RouterWithWebSockets();
routerWithWebSockets.router.get('/nonWSRoute', doSomething1); // processed as HTTP GET request
routerWithWebSockets.router.get('/wsRoute1', doSomething2); // processed as HTTP GET request
routerWithWebSockets.ws('/wsRoute1', (ws) => doSomethingWithWS1); // processed as Websocket upgrade request
routerWithWebSockets.ws('/wsRoute2', (ws) => doSomethingWithWS2); // processed as Websocket upgrade request
export default routerWithWebSockets.router;
1 Comment
Eliav Louski
thank you!!
app(req, res) totally did the trickBun has excellent Node.js compatibility and native WebSocket support (as an alternative):
import { z, message, createRouter } from "@ws-kit/zod";
import { serve } from "@ws-kit/bun";
const Ping = message("PING", { text: z.string() });
const Pong = message("PONG", { reply: z.string() });
const router = createRouter();
router.on(Ping, (ctx) => {
ctx.send(Pong, { reply: `Got: ${ctx.payload.text}` });
});
serve(router, { port: 3000 });