2

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 2

2

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;
Sign up to request clarification or add additional context in comments.

1 Comment

thank you!! app(req, res) totally did the trick
0

Bun 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 });

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.