---
url: https://fulmine.sndesign.it/examples/websocket.md
description: >-
  Native WebSocket routes with app.ws() in an Express app, with an upgrade hook
  to check auth before the socket opens.
---

# WebSockets next to your Express routes

`app.ws()` is a WebSocket route served by uWebSockets.js, in the same app as the HTTP routes. The `upgrade(req, res)` hook decides if the socket opens, for example after checking a token.

From a clone of the repository:

```sh
cd examples && npm install
node websocket.js
```

```js
// app.ws(): a WebSocket route served by uWS itself. The upgrade never reaches node, so
// server.on("upgrade") and the libraries built on it have nothing to hear, and this is what
// replaces them. The behavior object is uWS's, with one addition of this project's own.
//
//   node websocket.js   ->  http://localhost:3000  (the page opens the socket)
const express = require("fulmine.js"); // instead of require("express")

const app = express();

// what the upgrade hook hangs on the request, read back off the socket
/** @typedef {express.Request & { topic: string }} RoomRequest */
/** @typedef {express.FulmineSocket & { req: RoomRequest }} RoomSocket */

app.ws("/room/:id", {
    // uWS's own settings, passed through untouched
    idleTimeout: 120,
    maxPayloadLength: 4 * 1024,

    // this project's addition: it runs before the handshake with the request and response an
    // ordinary route gets, so a token, a session or a header decides whether the socket opens.
    // Answering the response declines the upgrade, and returning a promise holds the handshake
    // until it settles, which is what an authentication lookup needs
    /** @param {RoomRequest} req */
    upgrade(req, res) {
        if (!/^[a-z0-9-]{1,24}$/.test(String(req.params.id))) {
            return res.sendStatus(400);
        }
        // anything left on the request is there for the socket's whole life, as ws.req
        req.topic = `room:${req.params.id}`;
    },

    /** @param {RoomSocket} ws */
    open(ws) {
        ws.subscribe(ws.req.topic);
        ws.send(`you are in ${ws.req.params.id}, ${app.numSubscribers(ws.req.topic)} here`);
    },

    /** @param {RoomSocket} ws */
    message(ws, message, isBinary) {
        app.publish(ws.req.topic, message, isBinary);
    },

    close(ws, code, message) {
        console.log(`left ${ws.req.params.id}`);
    }
});

// broadcasting from outside a socket, which is what app.publish is for
setInterval(() => app.publish("room:lobby", `it is ${new Date().toISOString()}`), 10_000).unref();

// a WebSocket route and an ordinary route can share a path: the upgrade goes to the first, a
// plain GET goes through normal routing
app.get("/", (req, res) => {
    res.type("html").send(`<!doctype html>
<meta charset="utf-8"><title>app.ws()</title>
<pre id="log"></pre>
<script>
  const ws = new WebSocket(location.origin.replace("http", "ws") + "/room/lobby");
  ws.onmessage = (e) => (log.textContent += e.data + "\\n");
  ws.onopen = () => ws.send("hello from the browser");
</script>`);
});

app.listen(3000, () => console.log("http://localhost:3000"));

```

See also: [WebSockets](/websockets).
