WebSockets
Declare typed WebSocket routes in the contract.
WebSocket routes use the same contract tree as HTTP routes.
They must use method: "GET" and options: { mode: "websocket" }.
watch: {
method: "GET",
path: "/todos/:id/watch",
options: {
mode: "websocket",
},
request: {
params: {
id: z.string(),
},
},
messages: {
client: clientMessageSchema,
server: serverMessageSchema,
},
}
Contract
export const api = router({
todos: {
watch: {
method: "GET",
path: "/todos/:id/watch",
options: {
mode: "websocket",
},
request: {
params: {
id: z.string(),
},
},
messages: {
client: z.object({
type: z.literal("ping"),
}),
server: z.discriminatedUnion("type", [
z.object({
type: z.literal("changed"),
id: z.string(),
completed: z.boolean(),
}),
z.object({
type: z.literal("pong"),
}),
]),
},
},
},
});
Server Handler
Server handlers receive typed socket context.
const routes = router(api, {
todos: {
watch({ id, context }) {
context.socket.send({
type: "changed",
id,
completed: false,
});
context.socket.onMessage((message) => {
if (message.type === "ping") {
context.socket.send({
type: "pong",
});
}
});
},
},
});
Incoming messages are parsed and validated before delivery. Invalid incoming messages close the connection.
Outgoing server messages are validated before being sent. They are sent as JSON, so schema output must still make sense as a JSON message.
Client
WebSocket client routes expose openConnection().
const socket = client.todos.watch.openConnection({
id: "todo_1",
});
socket.onMessage((message) => {
if (message.type === "changed") {
console.log(message.completed);
}
});
socket.send({
type: "ping",
});
By default, incoming server messages are JSON-parsed but trusted by the client.
If the client enables validateResponses, incoming server messages are also
validated on the client. See
HTTP and Schema I/O gotchas before
using that option with transformed server message schemas.
Registration
Adapters register WebSocket routes with explicit WebSocket options.
Install ws when using WebSocket routes.
pnpm add wsExpress uses a Node HTTP server for the upgrade event and a ws WebSocket
server to accept sockets. Create the Node server from the Express app, then pass
both servers in webSocket.
beforeUpgrade runs after request validation and before the socket is accepted.
Return an upgrade rejection to deny the connection, or read and attach data to
context.req for the WebSocket handler.
import { createServer } from "node:http";
import express from "express";
import { WebSocketServer } from "ws";
import { registerRoutes } from "@rest-rpc/express";
const expressApp = express();
const nodeServer = createServer(expressApp);
const webSocketServer = new WebSocketServer({ noServer: true });
registerRoutes(expressApp, routes, {
webSocket: {
server: nodeServer,
webSocketServer,
beforeUpgrade({ context, route, request }) {
context.req;
route;
request;
return undefined;
},
},
});
nodeServer.listen(3000);Hono gets upgradeWebSocket from the Hono runtime adapter. Create the Hono app
normally, import upgradeWebSocket from your runtime, then pass both to
registerRoutes.
beforeUpgrade runs after request validation and before the socket is accepted.
Return an upgrade rejection to deny the connection, or read and attach data to
context.c for the WebSocket handler.
import { Hono } from "hono";
import { upgradeWebSocket } from "hono/cloudflare-workers";
import { registerRoutes } from "@rest-rpc/hono";
const honoApp = new Hono();
registerRoutes(honoApp, routes, {
webSocket: {
upgradeWebSocket,
beforeUpgrade({ context, route, request }) {
context.c;
route;
request;
return undefined;
},
},
});Install @fastify/websocket when using WebSocket routes.
pnpm add @fastify/websocketFastify requires @fastify/websocket. Create the Fastify app, register the
plugin before registering routes, then pass WebSocket options to
registerRoutes.
beforeUpgrade runs after request validation and before the socket is accepted.
Return an upgrade rejection to deny the connection, or read and attach data to
context.req for the WebSocket handler.
import websocket from "@fastify/websocket";
import Fastify from "fastify";
import { registerRoutes } from "@rest-rpc/fastify";
const fastifyApp = Fastify();
await fastifyApp.register(websocket);
registerRoutes(fastifyApp, routes, {
webSocket: {
beforeUpgrade({ context, route, request }) {
context.req;
route;
request;
return undefined;
},
},
});