> Documentation for Redweb 0.13.0. Install that exact version when following these examples.

# Serve HTTP and WebSockets on one port

Build an Express endpoint and a raw WebSocket route on the same Node listener. Use this when an existing HTTP application needs socket endpoints without a second port or a separate web framework. This guide demonstrates server composition, not a rendered chat interface.

## Explain it like I'm five

Imagine one front door with two signs. Ordinary HTTP visitors ask for a page or JSON response. WebSocket visitors ask to keep a conversation open. Both use the same door, but different route and handler classes decide what happens inside. One owner is responsible for closing the building.

## Follow the design

1. `HttpServer({ listen: false })` builds the Express application and Node server without opening a port. `/health` answers ordinary HTTP requests; `publicPaths: []` avoids exposing an incidental working-directory folder.
2. Pass that Node server to `SocketServer`, alongside the `/chat` route. `listen: true` explicitly starts the supplied listener; `closeServerOnShutdown: true` assigns its cleanup to the socket service.
3. The URL selects `ChatRoute`. A raw JSON message with `type: "hello"` selects `Hello`; there is no secondary action dispatcher.
4. `createApp()` returns the one cleanup owner. Its `shutdown()` processes route failures and still closes the shared HTTP peers. The generated entrypoint helper adds bounded process shutdown without another handwritten signal policy.

The framework ordinarily leaves supplied listeners caller-owned. These explicit flags are a choice made by this starter, not a change to that default. Use [migration and ownership guidance](/docs/reference/0.13.0/migration.md) when adapting an existing application; do not let two independent services compete to close the same listener.

## Check that it works

Request `http://127.0.0.1:8181/health` and expect `{"ok":true}`. Open a WebSocket to `ws://127.0.0.1:8181/chat`, send `{"type":"hello"}`, and expect `{"type":"hello","message":"Hello from the server!"}`. An unknown socket path is rejected rather than sent to a catch-all handler.

The [shipped tests](/docs/reference/0.13.0/recipes/http-ws/files/test/app.test.cjs) use real HTTP and WebSocket clients on one ephemeral port. They also leave an HTTP request incomplete, repeat shutdown, and deliberately fail an application route's cleanup to confirm the listener still closes. Shared lifecycle tests cover process-level shutdown failures; the package gate repeats the compiled application checks with source removed.

## Before public deployment

The starter deliberately binds loopback. Configure the deployment bind address and HTTPS/WSS termination, trusted origins, identity, authorization and capacity limits before exposing it. `/health` proves liveness, not readiness to accept game traffic or completion of durable work. Forced shutdown closes transports; it does not guarantee delivery, transaction completion or storage.

For shared validation and inferred payloads, use the [typed WebSocket guide](/docs/reference/0.13.0/guides/typed-websockets.md). For UI updates driven by server-side components, use the [chatroom guide](/docs/reference/0.13.0/guides/chatroom.md). See [operations and deployment boundaries](/docs/reference/0.13.0/operations.md) before adding proxies or multiple workers.

## Build and run the complete application

```sh
npx --yes redweb@0.13.0 init my-http-ws --template http-ws
cd my-http-ws
npm install --save-exact redweb@0.13.0
npm test
npm run dev
```

The [complete http-ws recipe](/docs/reference/0.13.0/recipes/http-ws.md) contains every generated file, its real acceptance tests, and deployment instructions. The source below is one of those files, not a standalone program; initialize the whole project before modifying it.

## Source walkthrough: src/app.tsx

```tsx
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);
```
