Node.js
serve function from crossws/server to automatically integrate crossws with Node.js!To manually integrate crossws with your Node.js HTTP server, you need to connect the upgrade event to the handleUpgrade method returned from the adapter. crossws uses a prebundled version of ws.
import { createServer } from "node:http";
import crossws from "crossws/adapters/node";
const ws = crossws({
hooks: {
message: console.log,
},
});
const server = createServer((req, res) => {
res.end(
`<script>new WebSocket("ws://localhost:3000").addEventListener('open', (e) => e.target.send("Hello from client!"));</script>`,
);
}).listen(3000);
server.on("upgrade", (req, socket, head) => {
if (req.headers.upgrade === "websocket") {
ws.handleUpgrade(req, socket, head);
}
});
test/fixture/node.ts for demo and src/adapters/node.ts for implementation.Delegating to an existing Node.js upgrade handler
If you already have a Node.js WebSocket library that exposes a raw (req, socket, head) upgrade handler (e.g. ws, socket.io, express-ws), you can route to it through crossws using fromNodeUpgradeHandler. This lets you keep crossws's upgrade-time request handling while delegating the WebSocket lifecycle to your existing library.
import { WebSocketServer } from "ws";
import { fromNodeUpgradeHandler } from "crossws/adapters/node";
import { serve } from "crossws/server/node";
const wss = new WebSocketServer({ noServer: true });
wss.on("connection", (ws) => {
ws.on("message", (data) => ws.send(data));
});
serve({
fetch: () => new Response("ok"),
websocket: fromNodeUpgradeHandler((req, socket, head) => {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
}),
});
The underlying handler takes full ownership of the socket, so crossws's other lifecycle hooks (open, message, close, error) are not invoked for connections routed through it — manage the WebSocket lifecycle inside your own library as usual.
fromNodeUpgradeHandler only works on the Node.js runtime, and must be used via the crossws node server plugin so the request carries runtime.node.upgrade.{socket, head}.uWebSockets
You can alternatively use uWebSockets.js for Node.js servers.
First add uNetworking/uWebSockets.js as a dependency.
import { App } from "uWebSockets.js";
import crossws from "crossws/adapters/uws";
const ws = crossws({
hooks: {
message: console.log,
},
});
const server = App().ws("/*", ws.websocket);
server.get("/*", (res, req) => {
res.writeStatus("200 OK").writeHeader("Content-Type", "text/html");
res.end(
`<script>new WebSocket("ws://localhost:3000").addEventListener('open', (e) => e.target.send("Hello from client!"));</script>`,
);
});
server.listen(3001, () => {
console.log("Listening to port 3001");
});
test/fixture/node-uws.ts for demo and src/adapters/node-uws.ts for implementation.Socket.IO
Socket.IO speaks two transports: a WebSocket upgrade and an HTTP long-polling fallback served under /socket.io/. Both can be routed through crossws by combining fromNodeUpgradeHandler with srvx's fetchNodeHandler helper to forward the polling path (and the optional client bundle at /socket.io/socket.io.js) to socket.io's own Node-style (req, res) listener:
import { createServer } from "node:http";
import { Server as SocketIOServer } from "socket.io";
import { fromNodeUpgradeHandler } from "crossws/adapters/node";
import { serve } from "crossws/server/node";
import { fetchNodeHandler } from "srvx/node";
// Attach socket.io to a throwaway http.Server — it never `.listen()`s,
// but the dummy server now holds socket.io's own `request` listener,
// which handles the client bundle AND delegates polling to engine.io.
const dummyServer = createServer();
const io = new SocketIOServer(dummyServer, { serveClient: true });
const [socketIoRequestListener] = dummyServer.listeners("request");
io.on("connection", (socket) => {
socket.emit("welcome", { id: socket.id });
socket.on("message", (text) => io.emit("message", { from: socket.id, text }));
});
const server = serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname.startsWith("/socket.io/")) {
return fetchNodeHandler(socketIoRequestListener, req);
}
return new Response("ok");
},
websocket: fromNodeUpgradeHandler((req, socket, head) => {
io.engine.handleUpgrade(req, socket, head);
}),
});
await server.ready();
If you only need the WebSocket transport, force the client to skip polling with io(url, { transports: ["websocket"] }) and you can drop the fetch-side delegation entirely.
examples/socket.io for a runnable demo including a browser client.