|
| 1 | +--- |
| 2 | +icon: ri:broadcast-fill |
| 3 | +--- |
| 4 | + |
| 5 | +# WebSocket |
| 6 | + |
| 7 | +> Nitro provides cross-platform WebSocket support powered by [CrossWS](https://crossws.h3.dev/) and [H3](https://h3.dev/). |
| 8 | +
|
| 9 | +WebSocket enables real-time, bidirectional communication between client and server. Nitro's WebSocket integration works across all supported deployment targets including Node.js, Bun, Deno, and Cloudflare Workers. |
| 10 | + |
| 11 | +:read-more{to="https://crossws.h3.dev/" title="CrossWS Documentation"} |
| 12 | + |
| 13 | +## Enable WebSocket |
| 14 | + |
| 15 | +Enable WebSocket support in your Nitro configuration: |
| 16 | + |
| 17 | +::code-group |
| 18 | +```ts [nitro.config.ts] |
| 19 | +import { defineConfig } from "nitro"; |
| 20 | + |
| 21 | +export default defineConfig({ |
| 22 | + features: { |
| 23 | + websocket: true, |
| 24 | + }, |
| 25 | +}); |
| 26 | +``` |
| 27 | +:: |
| 28 | + |
| 29 | +## Usage |
| 30 | + |
| 31 | +Create a WebSocket handler using `defineWebSocketHandler` and export it from a route file. WebSocket handlers follow the same [file-based routing](/docs/routing) as regular request handlers. |
| 32 | + |
| 33 | +```ts [routes/_ws.ts] |
| 34 | +import { defineWebSocketHandler } from "nitro"; |
| 35 | + |
| 36 | +export default defineWebSocketHandler({ |
| 37 | + open(peer) { |
| 38 | + console.log("Connected:", peer.id); |
| 39 | + }, |
| 40 | + message(peer, message) { |
| 41 | + console.log("Message:", message.text()); |
| 42 | + peer.send("Hello from server!"); |
| 43 | + }, |
| 44 | + close(peer, details) { |
| 45 | + console.log("Disconnected:", peer.id, details.code, details.reason); |
| 46 | + }, |
| 47 | + error(peer, error) { |
| 48 | + console.error("Error:", error); |
| 49 | + }, |
| 50 | +}); |
| 51 | +``` |
| 52 | + |
| 53 | +::tip |
| 54 | +You can use any route path for WebSocket handlers. For example, `routes/chat.ts` handles WebSocket connections on `/chat`. |
| 55 | +:: |
| 56 | + |
| 57 | +### Connecting from the client |
| 58 | + |
| 59 | +Use the browser's [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) to connect: |
| 60 | + |
| 61 | +```js |
| 62 | +const ws = new WebSocket("ws://localhost:3000/_ws"); |
| 63 | + |
| 64 | +ws.addEventListener("open", () => { |
| 65 | + console.log("Connected!"); |
| 66 | + ws.send("Hello from client!"); |
| 67 | +}); |
| 68 | + |
| 69 | +ws.addEventListener("message", (event) => { |
| 70 | + console.log("Received:", event.data); |
| 71 | +}); |
| 72 | +``` |
| 73 | + |
| 74 | +## Hooks |
| 75 | + |
| 76 | +WebSocket handlers accept the following lifecycle hooks: |
| 77 | + |
| 78 | +### `upgrade` |
| 79 | + |
| 80 | +Called before the WebSocket connection is established. Use it to authenticate requests, set the namespace, or attach context data to the peer. |
| 81 | + |
| 82 | +```ts [routes/chat.ts] |
| 83 | +import { defineWebSocketHandler } from "nitro"; |
| 84 | + |
| 85 | +export default defineWebSocketHandler({ |
| 86 | + upgrade(request) { |
| 87 | + const url = new URL(request.url); |
| 88 | + const token = url.searchParams.get("token"); |
| 89 | + if (!isValidToken(token)) { |
| 90 | + throw new Response("Unauthorized", { status: 401 }); |
| 91 | + } |
| 92 | + return { |
| 93 | + context: { userId: getUserId(token) }, |
| 94 | + }; |
| 95 | + }, |
| 96 | + open(peer) { |
| 97 | + console.log("User connected:", peer.context.userId); |
| 98 | + }, |
| 99 | + // ... |
| 100 | +}); |
| 101 | +``` |
| 102 | + |
| 103 | +The `upgrade` hook can return an object with: |
| 104 | + |
| 105 | +| Property | Type | Description | |
| 106 | +|---|---|---| |
| 107 | +| `headers` | `HeadersInit` | Response headers to include in the upgrade response | |
| 108 | +| `namespace` | `string` | Override the pub/sub namespace for this connection | |
| 109 | +| `context` | `object` | Data attached to `peer.context` | |
| 110 | + |
| 111 | +Throw a `Response` to reject the upgrade. |
| 112 | + |
| 113 | +### `open` |
| 114 | + |
| 115 | +Called when a WebSocket connection is established and the peer is ready to send and receive messages. |
| 116 | + |
| 117 | +```ts |
| 118 | +open(peer) { |
| 119 | + peer.send("Welcome!"); |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +### `message` |
| 124 | + |
| 125 | +Called when a message is received from a peer. |
| 126 | + |
| 127 | +```ts |
| 128 | +message(peer, message) { |
| 129 | + const text = message.text(); |
| 130 | + const data = message.json(); |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | +### `close` |
| 135 | + |
| 136 | +Called when a WebSocket connection is closed. Receives a `details` object with optional `code` and `reason`. |
| 137 | + |
| 138 | +```ts |
| 139 | +close(peer, details) { |
| 140 | + console.log(`Closed: ${details.code} - ${details.reason}`); |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +### `error` |
| 145 | + |
| 146 | +Called when an error occurs on the WebSocket connection. |
| 147 | + |
| 148 | +```ts |
| 149 | +error(peer, error) { |
| 150 | + console.error("WebSocket error:", error); |
| 151 | +} |
| 152 | +``` |
| 153 | + |
| 154 | +## Peer |
| 155 | + |
| 156 | +The `peer` object represents a connected WebSocket client. It is available in all hooks except `upgrade`. |
| 157 | + |
| 158 | +### Properties |
| 159 | + |
| 160 | +| Property | Type | Description | |
| 161 | +|---|---|---| |
| 162 | +| `id` | `string` | Unique identifier for this peer | |
| 163 | +| `namespace` | `string` | Pub/sub namespace this peer belongs to | |
| 164 | +| `context` | `object` | Arbitrary context data set during `upgrade` | |
| 165 | +| `request` | `Request` | The original upgrade request | |
| 166 | +| `peers` | `Set<Peer>` | All connected peers in the same namespace | |
| 167 | +| `topics` | `Set<string>` | Topics this peer is subscribed to | |
| 168 | +| `remoteAddress` | `string?` | Client IP address (adapter-dependent) | |
| 169 | +| `websocket` | `WebSocket` | The underlying WebSocket instance | |
| 170 | + |
| 171 | +### Methods |
| 172 | + |
| 173 | +#### `peer.send(data, options?)` |
| 174 | + |
| 175 | +Send a message directly to this peer. Accepts strings, objects (serialized as JSON), or binary data. |
| 176 | + |
| 177 | +```ts |
| 178 | +peer.send("Hello!"); |
| 179 | +peer.send({ type: "greeting", text: "Hello!" }); |
| 180 | +``` |
| 181 | + |
| 182 | +#### `peer.subscribe(topic)` |
| 183 | + |
| 184 | +Subscribe this peer to a pub/sub topic. |
| 185 | + |
| 186 | +```ts |
| 187 | +peer.subscribe("notifications"); |
| 188 | +``` |
| 189 | + |
| 190 | +#### `peer.unsubscribe(topic)` |
| 191 | + |
| 192 | +Unsubscribe this peer from a topic. |
| 193 | + |
| 194 | +```ts |
| 195 | +peer.unsubscribe("notifications"); |
| 196 | +``` |
| 197 | + |
| 198 | +#### `peer.publish(topic, data, options?)` |
| 199 | + |
| 200 | +Broadcast a message to all peers subscribed to a topic within the same namespace. The publishing peer does **not** receive the message. |
| 201 | + |
| 202 | +```ts |
| 203 | +peer.publish("chat", { user: "Alice", text: "Hello everyone!" }); |
| 204 | +``` |
| 205 | + |
| 206 | +#### `peer.close(code?, reason?)` |
| 207 | + |
| 208 | +Gracefully close the WebSocket connection. |
| 209 | + |
| 210 | +```ts |
| 211 | +peer.close(1000, "Normal closure"); |
| 212 | +``` |
| 213 | + |
| 214 | +#### `peer.terminate()` |
| 215 | + |
| 216 | +Immediately terminate the connection without sending a close frame. |
| 217 | + |
| 218 | +## Message |
| 219 | + |
| 220 | +The `message` object in the `message` hook provides methods to read the incoming data in different formats. |
| 221 | + |
| 222 | +| Method | Return Type | Description | |
| 223 | +|---|---|---| |
| 224 | +| `text()` | `string` | Message as a UTF-8 string | |
| 225 | +| `json()` | `T` | Message parsed as JSON | |
| 226 | +| `uint8Array()` | `Uint8Array` | Message as a byte array | |
| 227 | +| `arrayBuffer()` | `ArrayBuffer` | Message as an ArrayBuffer | |
| 228 | +| `blob()` | `Blob` | Message as a Blob | |
| 229 | + |
| 230 | +```ts |
| 231 | +message(peer, message) { |
| 232 | + // Parse as text |
| 233 | + const text = message.text(); |
| 234 | + |
| 235 | + // Parse as typed JSON |
| 236 | + const data = message.json<{ type: string; payload: unknown }>(); |
| 237 | +} |
| 238 | +``` |
| 239 | + |
| 240 | +## Pub/Sub |
| 241 | + |
| 242 | +Pub/sub (publish/subscribe) enables broadcasting messages to groups of connected peers through topics. Peers subscribe to topics and receive messages published to those topics. |
| 243 | + |
| 244 | +```ts [routes/chat.ts] |
| 245 | +import { defineWebSocketHandler } from "nitro"; |
| 246 | + |
| 247 | +export default defineWebSocketHandler({ |
| 248 | + open(peer) { |
| 249 | + peer.subscribe("chat"); |
| 250 | + peer.publish("chat", { system: `${peer} joined the chat` }); |
| 251 | + peer.send({ system: "Welcome to the chat!" }); |
| 252 | + }, |
| 253 | + message(peer, message) { |
| 254 | + // Broadcast to all other subscribers |
| 255 | + peer.publish("chat", { |
| 256 | + user: peer.toString(), |
| 257 | + text: message.text(), |
| 258 | + }); |
| 259 | + // Echo back to sender |
| 260 | + peer.send({ user: "You", text: message.text() }); |
| 261 | + }, |
| 262 | + close(peer) { |
| 263 | + peer.publish("chat", { system: `${peer} left the chat` }); |
| 264 | + }, |
| 265 | +}); |
| 266 | +``` |
| 267 | + |
| 268 | +::note |
| 269 | +`peer.publish()` sends the message to all subscribers of the topic **except** the publishing peer. Use `peer.send()` to also send to the publisher. |
| 270 | +:: |
| 271 | + |
| 272 | +### Namespaces |
| 273 | + |
| 274 | +Namespaces provide isolated pub/sub groups for WebSocket connections. Each peer belongs to one namespace, and `peer.publish()` only broadcasts to peers within the same namespace. |
| 275 | + |
| 276 | +By default, the namespace is derived from the request URL pathname. This works naturally with [dynamic routes](/docs/routing#dynamic-routes) — each path gets its own isolated namespace: |
| 277 | + |
| 278 | +```ts [routes/rooms/[room].ts] |
| 279 | +import { defineWebSocketHandler } from "nitro"; |
| 280 | + |
| 281 | +export default defineWebSocketHandler({ |
| 282 | + open(peer) { |
| 283 | + peer.subscribe("messages"); |
| 284 | + peer.publish("messages", `${peer} joined ${peer.namespace}`); |
| 285 | + }, |
| 286 | + message(peer, message) { |
| 287 | + // Only reaches peers in the same room |
| 288 | + peer.publish("messages", `${peer}: ${message.text()}`); |
| 289 | + }, |
| 290 | + close(peer) { |
| 291 | + peer.publish("messages", `${peer} left`); |
| 292 | + }, |
| 293 | +}); |
| 294 | +``` |
| 295 | + |
| 296 | +In this example, clients connecting to `/rooms/game` are isolated from clients connecting to `/rooms/lobby` — each path is its own namespace. |
| 297 | + |
| 298 | +To override the default namespace, return a custom `namespace` from the `upgrade` hook: |
| 299 | + |
| 300 | +```ts [routes/chat.ts] |
| 301 | +import { defineWebSocketHandler } from "nitro"; |
| 302 | + |
| 303 | +export default defineWebSocketHandler({ |
| 304 | + upgrade(request) { |
| 305 | + // Group connections by a query parameter instead of the pathname |
| 306 | + const url = new URL(request.url); |
| 307 | + const channel = url.searchParams.get("channel") || "general"; |
| 308 | + return { |
| 309 | + namespace: `chat:${channel}`, |
| 310 | + }; |
| 311 | + }, |
| 312 | + open(peer) { |
| 313 | + peer.subscribe("messages"); |
| 314 | + peer.publish("messages", `${peer} joined`); |
| 315 | + }, |
| 316 | + message(peer, message) { |
| 317 | + peer.publish("messages", `${peer}: ${message.text()}`); |
| 318 | + }, |
| 319 | + close(peer) { |
| 320 | + peer.publish("messages", `${peer} left`); |
| 321 | + }, |
| 322 | +}); |
| 323 | +``` |
| 324 | + |
| 325 | +## Server-Sent Events (SSE) |
| 326 | + |
| 327 | +[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) provide a simpler alternative when you only need server-to-client streaming. Unlike WebSockets, SSE uses standard HTTP and supports automatic reconnection. |
| 328 | + |
| 329 | +```ts [routes/sse.ts] |
| 330 | +import { defineHandler } from "nitro"; |
| 331 | +import { createEventStream } from "nitro/h3"; |
| 332 | + |
| 333 | +export default defineHandler((event) => { |
| 334 | + const stream = createEventStream(event); |
| 335 | + |
| 336 | + const interval = setInterval(async () => { |
| 337 | + await stream.push(`Message @ ${new Date().toLocaleTimeString()}`); |
| 338 | + }, 1000); |
| 339 | + |
| 340 | + stream.onClosed(() => { |
| 341 | + clearInterval(interval); |
| 342 | + }); |
| 343 | + |
| 344 | + return stream.send(); |
| 345 | +}); |
| 346 | +``` |
| 347 | + |
| 348 | +Connect from the client using the [EventSource API](https://developer.mozilla.org/en-US/docs/Web/API/EventSource): |
| 349 | + |
| 350 | +```js |
| 351 | +const source = new EventSource("/sse"); |
| 352 | + |
| 353 | +source.onmessage = (event) => { |
| 354 | + console.log(event.data); |
| 355 | +}; |
| 356 | +``` |
| 357 | + |
| 358 | +### Structured messages |
| 359 | + |
| 360 | +SSE messages support optional `id`, `event`, and `retry` fields: |
| 361 | + |
| 362 | +```ts [routes/events.ts] |
| 363 | +import { defineHandler } from "nitro"; |
| 364 | +import { createEventStream } from "nitro/h3"; |
| 365 | + |
| 366 | +export default defineHandler((event) => { |
| 367 | + const stream = createEventStream(event); |
| 368 | + let id = 0; |
| 369 | + |
| 370 | + const interval = setInterval(async () => { |
| 371 | + await stream.push({ |
| 372 | + id: String(id++), |
| 373 | + event: "update", |
| 374 | + data: JSON.stringify({ value: Math.random() }), |
| 375 | + retry: 3000, |
| 376 | + }); |
| 377 | + }, 1000); |
| 378 | + |
| 379 | + stream.onClosed(() => { |
| 380 | + clearInterval(interval); |
| 381 | + }); |
| 382 | + |
| 383 | + return stream.send(); |
| 384 | +}); |
| 385 | +``` |
| 386 | + |
| 387 | +:read-more{to="https://h3.dev/" title="H3 Documentation"} |
0 commit comments