Skip to content

Commit 9de7c21

Browse files
committed
docs: add websocket guide (#4197)
1 parent 11d292e commit 9de7c21

3 files changed

Lines changed: 390 additions & 1 deletion

File tree

docs/.config/docs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ redirects:
1616
"/guide/getting-started": "/docs"
1717
"/guide/utils": "/docs/routing"
1818
"/guide/routing": "/docs/routing"
19-
"/guide/websocket": "/docs/routing"
19+
"/guide/websocket": "/docs/websocket"
2020
"/guide/cache": "/docs/cache"
2121
"/guide/storage": "/docs/storage"
2222
"/guide/database": "/docs/database"

docs/1.docs/50.websocket.md

Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
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

Comments
 (0)