WebSockets
Low-level WebSocket routes for custom real-time protocols
Creating a WebSocket Route
Create a WebSocket route in app/ws/ (note: different from app/sockets/):
// app/ws/custom/route.ts
import type { ServerWebSocket, WebSocketContext } from "@ademattos/bunbox";
export function onOpen(ws: ServerWebSocket, ctx: WebSocketContext) {
console.log("WebSocket opened");
// Send a welcome message
ws.send(JSON.stringify({ type: "welcome", message: "Connected!" }));
// Subscribe to route topic (auto-managed)
// Already subscribed to ctx.topic automatically
}
export function onMessage(
ws: ServerWebSocket,
message: string | Buffer,
ctx: WebSocketContext
) {
console.log("Received:", message);
// Echo back
ws.send(message);
// Broadcast to all connections on this route
ctx.broadcast(message);
}
export function onClose(ws: ServerWebSocket, ctx: WebSocketContext) {
console.log("WebSocket closed");
}This creates a WebSocket at /ws/custom.
WebSocket API
The ServerWebSocket provides Bun's native WebSocket API:
export function onMessage(ws: ServerWebSocket, message: string | Buffer, ctx: WebSocketContext) {
// Send message to this client
ws.send("Hello!");
ws.send(new Uint8Array([1, 2, 3]));
// Close connection
ws.close(1000, "Goodbye");
// Check connection state
console.log(ws.readyState);
console.log(ws.remoteAddress);
// Subscribe to topics
ws.subscribe("room-1");
ws.unsubscribe("room-1");
// Publish to topic
ws.publish("room-1", "Message for room 1");
// Check subscriptions
console.log(ws.subscriptions); // ["ws-custom", "room-1"]
console.log(ws.isSubscribed("room-1")); // true
}WebSocket Context
The WebSocketContext provides convenient broadcasting:
export function onMessage(
ws: ServerWebSocket,
message: string | Buffer,
ctx: WebSocketContext
) {
// Broadcast to all clients on this route (includes sender)
ctx.broadcast(message);
// Broadcast JSON
ctx.broadcastJSON({ type: "update", data: "..." });
// Get route topic
console.log(ctx.topic); // "ws-custom"
}Client-Side Usage
Connect from the browser:
// In browser
const ws = new WebSocket("ws://localhost:3000/ws/custom");
ws.onopen = () => {
console.log("Connected");
ws.send("Hello server!");
};
ws.onmessage = (event) => {
console.log("Received:", event.data);
};
ws.onclose = () => {
console.log("Disconnected");
};
ws.onerror = (error) => {
console.error("Error:", error);
};Connection Upgrade
Control who can connect with the upgrade function:
export function upgrade(req: Request): boolean | { data?: unknown } {
// Check authentication
const token = new URL(req.url).searchParams.get("token");
if (!token || !isValidToken(token)) {
return false; // Reject connection
}
// Accept and attach custom data
return {
data: {
userId: getUserFromToken(token),
connectedAt: Date.now(),
},
};
}
export function onOpen(ws: ServerWebSocket, ctx: WebSocketContext) {
// Access custom data
console.log(ws.data); // { userId: "...", connectedAt: ... }
}Pub/Sub Rooms
Use topics for room-based communication:
export function onMessage(
ws: ServerWebSocket,
message: string | Buffer,
ctx: WebSocketContext
) {
const data = JSON.parse(message.toString());
if (data.type === "join-room") {
ws.subscribe(`room-${data.roomId}`);
ws.send(JSON.stringify({ type: "joined", roomId: data.roomId }));
}
if (data.type === "room-message") {
// Send only to room members
ws.publish(`room-${data.roomId}`, JSON.stringify({
type: "message",
text: data.text,
}));
}
if (data.type === "leave-room") {
ws.unsubscribe(`room-${data.roomId}`);
}
}Binary Data
Send and receive binary data:
export function onMessage(
ws: ServerWebSocket,
message: string | Buffer,
ctx: WebSocketContext
) {
if (Buffer.isBuffer(message)) {
console.log("Received binary:", message.length, "bytes");
// Echo binary back
ws.send(message);
// Or send new binary
const buffer = new Uint8Array([1, 2, 3, 4]);
ws.send(buffer);
}
}Compression
Enable compression for large messages:
export function onMessage(ws: ServerWebSocket, message: string | Buffer, ctx: WebSocketContext) {
// Send with compression
ws.send("Large message...", true);
ws.publish("topic", "Large broadcast...", true);
}Corking
Batch multiple sends for better performance:
export function onMessage(ws: ServerWebSocket, message: string | Buffer, ctx: WebSocketContext) {
ws.cork(() => {
// All sends are batched into one frame
ws.send("Message 1");
ws.send("Message 2");
ws.send("Message 3");
});
}Complete Example
// app/ws/game/route.ts
import type { ServerWebSocket, WebSocketContext } from "@ademattos/bunbox";
interface Player {
id: string;
x: number;
y: number;
}
const players = new Map<string, Player>();
export function upgrade(req: Request) {
const url = new URL(req.url);
const playerId = url.searchParams.get("playerId");
if (!playerId) {
return false;
}
return {
data: { playerId },
};
}
export function onOpen(ws: ServerWebSocket, ctx: WebSocketContext) {
const playerId = (ws.data as any).playerId;
// Add player
players.set(playerId, { id: playerId, x: 0, y: 0 });
// Send current game state
ws.send(JSON.stringify({
type: "init",
players: Array.from(players.values()),
}));
// Notify others
ctx.broadcastJSON({
type: "player-joined",
player: players.get(playerId),
});
}
export function onMessage(
ws: ServerWebSocket,
message: string | Buffer,
ctx: WebSocketContext
) {
const data = JSON.parse(message.toString());
const playerId = (ws.data as any).playerId;
if (data.type === "move") {
const player = players.get(playerId);
if (player) {
player.x = data.x;
player.y = data.y;
// Broadcast position update
ctx.broadcastJSON({
type: "player-moved",
playerId,
x: data.x,
y: data.y,
});
}
}
}
export function onClose(ws: ServerWebSocket, ctx: WebSocketContext) {
const playerId = (ws.data as any).playerId;
// Remove player
players.delete(playerId);
// Notify others
ctx.broadcastJSON({
type: "player-left",
playerId,
});
}When to Use WebSockets
Use WebSocket routes (app/ws/) when you need:
- Custom binary protocols
- Low-level control over messages
- Integration with existing WebSocket clients
- Performance-critical applications
For most cases, use Socket routes instead, which provide:
- Type-safe protocols
- Structured messages
- Easier client integration
- Better DX with
useSockethook
Debugging
Log connection details:
export function onOpen(ws: ServerWebSocket, ctx: WebSocketContext) {
console.log("Connection from:", ws.remoteAddress);
console.log("Route topic:", ctx.topic);
console.log("Custom data:", ws.data);
}
export function onMessage(ws: ServerWebSocket, message: string | Buffer, ctx: WebSocketContext) {
console.log("Message size:", message.length);
console.log("Active subscriptions:", ws.subscriptions);
}
export function onClose(ws: ServerWebSocket, ctx: WebSocketContext, code?: number, reason?: string) {
console.log("Closed with code:", code);
console.log("Reason:", reason);
}Error Handling
Handle errors gracefully:
export function onMessage(
ws: ServerWebSocket,
message: string | Buffer,
ctx: WebSocketContext
) {
try {
const data = JSON.parse(message.toString());
// Process data...
} catch (error) {
console.error("Invalid message:", error);
ws.send(JSON.stringify({ type: "error", message: "Invalid message format" }));
}
}Close Codes
Use standard WebSocket close codes:
export function onMessage(ws: ServerWebSocket, message: string | Buffer, ctx: WebSocketContext) {
// Normal closure
ws.close(1000, "Done");
// Going away
ws.close(1001, "Server restart");
// Protocol error
ws.close(1002, "Invalid data");
// Unsupported data
ws.close(1003, "Text only");
// Policy violation
ws.close(1008, "Unauthorized");
}