WebSocket
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.
Enable WebSocket
Enable WebSocket support in your Nitro configuration:
import { defineConfig } from "nitro";
export default defineConfig({
features: {
websocket: true,
},
});
Usage
Create a WebSocket handler using defineWebSocketHandler and export it from a route file. WebSocket handlers follow the same file-based routing as regular request handlers.
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
open(peer) {
console.log("Connected:", peer.id);
},
message(peer, message) {
console.log("Message:", message.text());
peer.send("Hello from server!");
},
close(peer, details) {
console.log("Disconnected:", peer.id, details.code, details.reason);
},
error(peer, error) {
console.error("Error:", error);
},
});
routes/chat.ts handles WebSocket connections on /chat.Connecting from the client
Use the browser's WebSocket API to connect:
const ws = new WebSocket("ws://localhost:3000/_ws");
ws.addEventListener("open", () => {
console.log("Connected!");
ws.send("Hello from client!");
});
ws.addEventListener("message", (event) => {
console.log("Received:", event.data);
});
Hooks
WebSocket handlers accept the following lifecycle hooks:
upgrade
Called before the WebSocket connection is established. Use it to authenticate requests, set the namespace, or attach context data to the peer.
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
upgrade(request) {
const url = new URL(request.url);
const token = url.searchParams.get("token");
if (!isValidToken(token)) {
throw new Response("Unauthorized", { status: 401 });
}
return {
context: { userId: getUserId(token) },
};
},
open(peer) {
console.log("User connected:", peer.context.userId);
},
// ...
});
The upgrade hook can return an object with:
| Property | Type | Description |
|---|---|---|
headers | HeadersInit | Response headers to include in the upgrade response |
namespace | string | Override the pub/sub namespace for this connection |
context | object | Data attached to peer.context |
Throw a Response to reject the upgrade.
open
Called when a WebSocket connection is established and the peer is ready to send and receive messages.
open(peer) {
peer.send("Welcome!");
}
message
Called when a message is received from a peer.
message(peer, message) {
const text = message.text();
const data = message.json();
}
close
Called when a WebSocket connection is closed. Receives a details object with optional code and reason.
close(peer, details) {
console.log(`Closed: ${details.code} - ${details.reason}`);
}
error
Called when an error occurs on the WebSocket connection.
error(peer, error) {
console.error("WebSocket error:", error);
}
Peer
The peer object represents a connected WebSocket client. It is available in all hooks except upgrade.
Properties
| Property | Type | Description |
|---|---|---|
id | string | Unique identifier for this peer |
namespace | string | Pub/sub namespace this peer belongs to |
context | object | Arbitrary context data set during upgrade |
request | Request | The original upgrade request |
peers | Set<Peer> | All connected peers in the same namespace |
topics | Set<string> | Topics this peer is subscribed to |
remoteAddress | string? | Client IP address (adapter-dependent) |
websocket | WebSocket | The underlying WebSocket instance |
Methods
peer.send(data, options?)
Send a message directly to this peer. Accepts strings, objects (serialized as JSON), or binary data.
peer.send("Hello!");
peer.send({ type: "greeting", text: "Hello!" });
peer.subscribe(topic)
Subscribe this peer to a pub/sub topic.
peer.subscribe("notifications");
peer.unsubscribe(topic)
Unsubscribe this peer from a topic.
peer.unsubscribe("notifications");
peer.publish(topic, data, options?)
Broadcast a message to all peers subscribed to a topic within the same namespace. The publishing peer does not receive the message.
peer.publish("chat", { user: "Alice", text: "Hello everyone!" });
peer.close(code?, reason?)
Gracefully close the WebSocket connection.
peer.close(1000, "Normal closure");
peer.terminate()
Immediately terminate the connection without sending a close frame.
Message
The message object in the message hook provides methods to read the incoming data in different formats.
| Method | Return Type | Description |
|---|---|---|
text() | string | Message as a UTF-8 string |
json() | T | Message parsed as JSON |
uint8Array() | Uint8Array | Message as a byte array |
arrayBuffer() | ArrayBuffer | Message as an ArrayBuffer |
blob() | Blob | Message as a Blob |
message(peer, message) {
// Parse as text
const text = message.text();
// Parse as typed JSON
const data = message.json<{ type: string; payload: unknown }>();
}
Pub/Sub
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.
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
open(peer) {
peer.subscribe("chat");
peer.publish("chat", { system: `${peer} joined the chat` });
peer.send({ system: "Welcome to the chat!" });
},
message(peer, message) {
// Broadcast to all other subscribers
peer.publish("chat", {
user: peer.toString(),
text: message.text(),
});
// Echo back to sender
peer.send({ user: "You", text: message.text() });
},
close(peer) {
peer.publish("chat", { system: `${peer} left the chat` });
},
});
peer.publish() sends the message to all subscribers of the topic except the publishing peer. Use peer.send() to also send to the publisher.Namespaces
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.
By default, the namespace is derived from the request URL pathname. This works naturally with dynamic routes — each path gets its own isolated namespace:
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
open(peer) {
peer.subscribe("messages");
peer.publish("messages", `${peer} joined ${peer.namespace}`);
},
message(peer, message) {
// Only reaches peers in the same room
peer.publish("messages", `${peer}: ${message.text()}`);
},
close(peer) {
peer.publish("messages", `${peer} left`);
},
});
In this example, clients connecting to /rooms/game are isolated from clients connecting to /rooms/lobby — each path is its own namespace.
To override the default namespace, return a custom namespace from the upgrade hook:
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
upgrade(request) {
// Group connections by a query parameter instead of the pathname
const url = new URL(request.url);
const channel = url.searchParams.get("channel") || "general";
return {
namespace: `chat:${channel}`,
};
},
open(peer) {
peer.subscribe("messages");
peer.publish("messages", `${peer} joined`);
},
message(peer, message) {
peer.publish("messages", `${peer}: ${message.text()}`);
},
close(peer) {
peer.publish("messages", `${peer} left`);
},
});
Server-Sent Events (SSE)
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.
import { defineHandler } from "nitro";
import { createEventStream } from "nitro/h3";
export default defineHandler((event) => {
const stream = createEventStream(event);
const interval = setInterval(async () => {
await stream.push(`Message @ ${new Date().toLocaleTimeString()}`);
}, 1000);
stream.onClosed(() => {
clearInterval(interval);
});
return stream.send();
});
Connect from the client using the EventSource API:
const source = new EventSource("/sse");
source.onmessage = (event) => {
console.log(event.data);
};
Structured messages
SSE messages support optional id, event, and retry fields:
import { defineHandler } from "nitro";
import { createEventStream } from "nitro/h3";
export default defineHandler((event) => {
const stream = createEventStream(event);
let id = 0;
const interval = setInterval(async () => {
await stream.push({
id: String(id++),
event: "update",
data: JSON.stringify({ value: Math.random() }),
retry: 3000,
});
}, 1000);
stream.onClosed(() => {
clearInterval(interval);
});
return stream.send();
});