API Reference
Complete API documentation for Bun WebSocket Router.
WebSocketRouter
The main class for creating WebSocket routers.
Constructor
new WebSocketRouter<TData = unknown>()
Type Parameters:
TData
- Type of custom data stored in WebSocket connections
Methods
onOpen(handler)
Register a handler for new connections.
onOpen(handler: (context: OpenHandlerContext<TData>) => void | Promise<void>): this
Parameters:
handler
- Function called when a client connectscontext.ws
- The WebSocket instance with typed datacontext.send
- Type-safe send function
Example:
router.onOpen((ctx) => {
console.log(`Client ${ctx.ws.data.clientId} connected`);
// ctx.send() auto-adds timestamp to meta
ctx.send(WelcomeMessage, { text: "Welcome!" });
});
onMessage(schema, handler)
Register a handler for a specific message type.
onMessage<TPayload>(
schema: MessageSchema<TPayload>,
handler: (context: MessageContext<TPayload, TData>) => void | Promise<void>
): this
Parameters:
schema
- Message schema created withmessageSchema()
handler
- Function to handle messages of this type
Example:
import { publish } from "bun-ws-router/zod/publish";
router.onMessage(ChatMessage, async (ctx) => {
await saveMessage(ctx.payload);
// publish() validates and auto-adds timestamp before broadcasting
publish(ctx.ws, "chat", ChatMessage, ctx.payload);
});
onClose(handler)
Register a handler for disconnections.
onClose(
handler: (context: CloseHandlerContext<TData>) => void | Promise<void>
): this
Parameters:
handler
- Function called when a client disconnectscontext.ws
- The WebSocket instance with typed datacontext.code
- WebSocket close codecontext.reason
- Optional close reason stringcontext.send
- Type-safe send function (for cleanup broadcasts only, cannot send to closed connection)
Example:
router.onClose((ctx) => {
console.log(
`Client ${ctx.ws.data.clientId} disconnected: ${ctx.code} ${ctx.reason || "N/A"}`,
);
// Clean up user from rooms
const rooms = getUserRooms(ctx.ws.data.clientId);
rooms.forEach((roomId) => {
removeUserFromRoom(ctx.ws.data.clientId, roomId);
});
});
Note: The send
function is provided in the context but can only be used for broadcasting to other clients via publish()
. Sending directly to the disconnected client (ctx.ws
) will fail since the connection is closed.
addRoutes(router)
Merge routes from another router.
addRoutes(router: WebSocketRouter<TData>): this
Parameters:
router
- Router instance to merge routes from
Example:
const authRouter = new WebSocketRouter();
const chatRouter = new WebSocketRouter();
const mainRouter = new WebSocketRouter()
.addRoutes(authRouter)
.addRoutes(chatRouter);
websocket
(Property)
Get Bun WebSocket handlers.
get websocket(): WebSocketHandler<WebSocketData<TData>>
Returns: Object with WebSocket event handlers for Bun.serve()
Example:
Bun.serve({
port: 3000,
websocket: router.websocket,
});
createMessageSchema
Factory function that creates message schema utilities using your validator instance. Required since v0.4.0 to fix discriminated union support.
function createMessageSchema(validator: ZodLike | ValibotLike): {
messageSchema: MessageSchemaFunction;
createMessage: CreateMessageFunction;
ErrorMessage: MessageSchema;
ErrorCode: Enum;
MessageMetadataSchema: Schema;
};
Parameters:
validator
- Your Zod (z
) or Valibot (v
) instance
Returns:
messageSchema
- Function to create message schemascreateMessage
- Helper for client-side message creationErrorMessage
- Pre-defined error message schemaErrorCode
- Error code enum/picklistMessageMetadataSchema
- Base metadata schema
Example:
import { z } from "zod";
import { createMessageSchema } from "bun-ws-router/zod";
const { messageSchema, createMessage, ErrorMessage, ErrorCode } =
createMessageSchema(z);
messageSchema
Function for creating message schemas (obtained from createMessageSchema
).
Overloads
// Message without payload
function messageSchema<TType extends string>(
type: TType,
): MessageSchema<undefined>;
// Message with payload
function messageSchema<TType extends string, TPayload>(
type: TType,
schema: Schema<TPayload>,
): MessageSchema<TPayload>;
// Message with payload and custom metadata
function messageSchema<TType extends string, TPayload, TMeta>(
type: TType,
schema: Schema<TPayload>,
metaSchema: Schema<TMeta>,
): MessageSchema<TPayload>;
Parameters:
type
- Unique message type identifierschema
- Zod or Valibot schema for payload validationmetaSchema
- Optional schema for custom metadata (third parameter, not wrapped in options)
Returns: MessageSchema object with type information
Examples:
// First create the factory
const { messageSchema } = createMessageSchema(z);
// Simple message
const PingMessage = messageSchema("PING");
// With payload
const ChatMessage = messageSchema("CHAT_MESSAGE", { text: z.string() });
// With custom metadata
const TrackedMessage = messageSchema(
"TRACKED_ACTION",
{ action: z.string() },
{ correlationId: z.string() },
);
// Works with discriminated unions!
const MessageUnion = z.discriminatedUnion("type", [
PingMessage,
ChatMessage,
TrackedMessage,
]);
MessageContext
Context object passed to message handlers.
Properties
interface MessageContext<TPayload, TData = unknown> {
ws: ServerWebSocket<TData>;
type: string;
meta: {
timestamp?: number;
correlationId?: string;
[key: string]: unknown;
};
receivedAt: number;
send: SendFunction;
payload?: TPayload; // Only present if schema defines payload
}
ws
- WebSocket instance (always includesctx.ws.data.clientId
)type
- Message type literalmeta
- Validated metadata (timestamp, correlationId, custom fields)receivedAt
- Server timestamp (authoritative for server logic)send
- Type-safe send functionpayload
- Validated payload (only exists if schema defines it)
Key Points:
- Client ID:
ctx.ws.data.clientId
(UUID v7, always present) - Server timestamp:
ctx.receivedAt
(use for rate limiting, ordering, TTL) - Client timestamp:
ctx.meta.timestamp
(use for UI display only) - Publishing: Use
publish()
helper (see below) - Subscriptions:
ctx.ws.subscribe(topic)
/ctx.ws.unsubscribe(topic)
Methods
send(schema, payload?, meta?)
Send a message to the current client.
send<T>(schema: MessageSchema<T>, payload?: T, meta?: Record<string, unknown>): void
send(message: Message): void
Examples:
// Using schema
ctx.send(ErrorMessage, {
code: ErrorCode.RESOURCE_NOT_FOUND,
message: "User not found",
});
// Using raw message
ctx.send({
type: "PONG",
meta: { timestamp: Date.now() },
});
Helper Functions
publish()
Type-safe helper for broadcasting messages to WebSocket topics.
Import:
import { publish } from "bun-ws-router/zod/publish";
// or
import { publish } from "bun-ws-router/valibot/publish";
Signature:
publish<T>(
ws: ServerWebSocket,
topic: string,
schema: MessageSchema<T>,
payload: T,
metaOrOpts?: Partial<MessageMetadata> | { origin?: string; key?: string }
): boolean
Parameters:
ws
- WebSocket instance (usuallyctx.ws
)topic
- Topic name to publish toschema
- Message schema for validationpayload
- Message payload datametaOrOpts
- Optional metadata or options object:- As metadata:
{ correlationId?: string, ... }
- Custom metadata fields - As options:
{ origin?: string, key?: string }
- Sender tracking configuration
- As metadata:
Returns: true
if message was validated and published successfully, false
otherwise
Examples:
import { publish } from "bun-ws-router/zod/publish";
// Basic publish with type safety
router.onMessage(ChatMessage, (ctx) => {
publish(ctx.ws, "room:123", ChatMessage, {
text: "Hello everyone!",
roomId: "123",
});
});
// Publish with custom metadata
publish(
ctx.ws,
"notifications",
NotificationMessage,
{ text: "Update available" },
{ correlationId: "req-123" },
);
// Publish with sender tracking (origin option)
// Automatically injects senderId from ws.data.userId
publish(
ctx.ws,
"room:123",
ChatMessage,
{ text: "Hello" },
{ origin: "userId" }, // Looks up ws.data.userId and adds to meta.senderId
);
// Custom sender field name
publish(
ctx.ws,
"room:123",
ChatMessage,
{ text: "Hello" },
{ origin: "userId", key: "authorId" }, // Adds to meta.authorId instead
);
Origin Option:
The origin
option enables automatic sender tracking by extracting a value from ws.data
and injecting it into the message metadata:
origin: "userId"
- Readsws.data.userId
and adds it tometa.senderId
key: "authorId"
- Usesmeta.authorId
instead of defaultmeta.senderId
This is useful for:
- Tracking who sent a broadcast message
- Filtering messages by sender
- Implementing sender-based permissions
Why is publish()
standalone?
The publish()
helper is a standalone function (not ctx.publish()
) because:
- Validation - Validates messages against schema before broadcasting (security boundary)
- Auto-timestamp - Automatically adds
timestamp
tometa
(likectx.send()
) - Type safety - Full TypeScript inference for payload and meta
- Return value - Returns
boolean
for error handling (true if any client received message)
For raw publishing without validation or timestamps, use ctx.ws.publish()
directly.
WebSocket Methods
These methods are available on ctx.ws
(Bun's ServerWebSocket):
subscribe()
Subscribe to topics for receiving broadcasts.
ctx.ws.subscribe(...topics: string[]): void
Example:
ctx.ws.subscribe("room:123", "notifications");
unsubscribe()
Unsubscribe from a topic.
ctx.ws.unsubscribe(...topics: string[]): void
Example:
ctx.ws.unsubscribe("room:123");
Custom Connection Data
Access and modify custom connection data via ctx.ws.data
:
// Access custom data (read)
const userId = ctx.ws.data.userId;
const roles = ctx.ws.data.roles;
// Access clientId (always present, UUID v7)
const clientId = ctx.ws.data.clientId;
// Modify custom data (write)
ctx.ws.data.isAuthenticated = true;
ctx.ws.data.lastActivity = Date.now();
Note: The clientId
field is automatically generated (UUID v7) by the router during WebSocket upgrade and is always present in ws.data
.
createMessage() Helper
Helper function for creating validated WebSocket messages on the client side (obtained from createMessageSchema
).
function createMessage<T extends MessageSchemaType>(
schema: T,
payload: T["shape"]["payload"] extends ZodType
? z.infer<T["shape"]["payload"]>
: undefined,
meta?: Partial<z.infer<T["shape"]["meta"]>>,
): SafeParseReturnType;
Parameters:
schema
- Message schema created withmessageSchema()
payload
- Message payload (type inferred from schema)meta
- Optional metadata to include
Returns:
A Zod/Valibot SafeParseReturnType
with either:
{ success: true, data: Message }
- Valid message{ success: false, error: ZodError }
- Validation errors
Example:
import { z } from "zod";
import { createMessageSchema } from "bun-ws-router/zod";
const { messageSchema, createMessage } = createMessageSchema(z);
const JoinMessage = messageSchema("JOIN", {
roomId: z.string(),
});
// Client-side usage
const message = createMessage(JoinMessage, { roomId: "general" });
if (message.success) {
ws.send(JSON.stringify(message.data));
} else {
console.error("Validation failed:", message.error);
}
// With metadata
const tracked = createMessage(
RequestMessage,
{ action: "fetch" },
{ correlationId: "req-123" },
);
ErrorCode and ErrorMessage
Standard error handling utilities (obtained from createMessageSchema
).
ErrorCode
Standard error codes for consistent error handling across your application.
// Zod version
const ErrorCode = z.enum([
"INVALID_MESSAGE_FORMAT",
"VALIDATION_FAILED",
"UNSUPPORTED_MESSAGE_TYPE",
"AUTHENTICATION_FAILED",
"AUTHORIZATION_FAILED",
"RESOURCE_NOT_FOUND",
"RATE_LIMIT_EXCEEDED",
"INTERNAL_SERVER_ERROR",
]);
// Valibot version
const ErrorCode = v.picklist([
"INVALID_MESSAGE_FORMAT",
"VALIDATION_FAILED",
"UNSUPPORTED_MESSAGE_TYPE",
"AUTHENTICATION_FAILED",
"AUTHORIZATION_FAILED",
"RESOURCE_NOT_FOUND",
"RATE_LIMIT_EXCEEDED",
"INTERNAL_SERVER_ERROR",
]);
Error Code Reference:
Code | Description | Common Use Cases |
---|---|---|
INVALID_MESSAGE_FORMAT | Message isn't valid JSON or lacks required structure | Malformed messages, parsing errors |
VALIDATION_FAILED | Message failed schema validation | Invalid payload data |
UNSUPPORTED_MESSAGE_TYPE | No handler registered for message type | Unknown message types |
AUTHENTICATION_FAILED | Authentication required or token invalid | Login failures, expired tokens |
AUTHORIZATION_FAILED | Insufficient permissions | Access control violations |
RESOURCE_NOT_FOUND | Resource not found | Missing users, rooms, items |
RATE_LIMIT_EXCEEDED | Too many requests | Rate limiting, spam prevention |
INTERNAL_SERVER_ERROR | Server error | Unexpected errors, bugs |
ErrorMessage
Pre-defined error message schema:
const ErrorMessage = messageSchema("ERROR", {
code: ErrorCode,
message: z.string().optional(), // or v.optional(v.string())
context: z.record(z.string(), z.any()).optional(),
});
Usage:
const { ErrorMessage, ErrorCode } = createMessageSchema(z);
ctx.send(ErrorMessage, {
code: "VALIDATION_FAILED",
message: "Invalid input",
context: { field: "email" },
});
TypeScript Types
Message
interface Message<T = unknown> {
type: string;
meta: {
timestamp?: number;
correlationId?: string;
};
payload?: T;
}
WebSocketData
interface WebSocketData<T = unknown> {
clientId: string; // UUID v7, auto-generated by router
} & T
MessageSchema
interface MessageSchema<TPayload> {
type: string;
schema?: Schema<TPayload>;
}