HTTP and WebSockets on one listener
Documentation for Redweb 0.13.2. Install that exact version when following these examples.
Build the Express side without binding, attach route classes to the same Node server, and explicitly give the socket service responsibility for listening and cleanup.
Use the complete http-ws recipe for setup, files, and tests.
import { BaseHandler, HttpServer, METHODS, SocketRoute, SocketServer, type RedWebSocket, type SocketServerOptions } from 'redweb';
import { runApp } from './run-app';
export class Hello extends BaseHandler {
constructor() { super('hello'); }
onMessage(socket: RedWebSocket) {
socket.sendJson({ type: 'hello', message: 'Hello from the server!' });
}
}
export class ChatRoute extends SocketRoute {
constructor() {
super({ path: '/chat', handlers: [Hello], allowDuplicateConnections: true });
}
}
export function createApp(options: Pick<SocketServerOptions, 'port' | 'bind' | 'logger'> = {}) {
const http = new HttpServer({
listen: false,
publicPaths: [],
services: [{ serviceName: '/health', method: METHODS.GET, function: (_req, res) => res.json({ ok: true }) }],
});
return new SocketServer({
port: options.port ?? Number(process.env.PORT ?? 8181),
bind: options.bind ?? '127.0.0.1',
logger: options.logger,
server: http.server,
routes: [ChatRoute],
listen: true,
closeServerOnShutdown: true, // One owner closes routes and the shared HTTP listener.
});
}
if (require.main === module) runApp(createApp);Notes and boundaries
- GET /health returns JSON; ws://127.0.0.1:8181/chat accepts {"type":"hello"}. The HTTP endpoint reports liveness, not readiness.
- Use the complete http-ws starter for compiler configuration, the shared entrypoint helper, and actual HTTP/WebSocket tests. It binds loopback for local development.
- The socket service explicitly owns shared-listener cleanup with closeServerOnShutdown: true. Call its shutdown() rather than a second HTTP shutdown sequence. Configure authentication, trusted origins, limits, and HTTPS/WSS before public deployment.