TypeScript Server
The ts-server target generates a framework-agnostic server using the Web Standard Fetch API
(Request/Response). It works with Bun, Deno,
Express, Cloudflare Workers, and any runtime that supports the Fetch API.
Quick Start
1. Define your contract
If you ran xrpc init, you already have this. Otherwise, create a contract file
(see API Contract):
// contract.ts
import { z } from 'zod';
import { createRouter, group, query, mutation } from 'xrpckit';
const example = group("example", {
hello: query({
input: z.object({ name: z.string().min(1).max(100) }),
output: z.object({ message: z.string() }),
}),
echo: mutation({
input: z.object({ text: z.string().max(1000) }),
output: z.object({ echoed: z.string() }),
}),
});
export const router = createRouter({ example }); 2. Generate
xrpc generate --targets ts-server
This produces two files in <output>/xrpc/:
types.ts— Zod schemas and inferred TypeScript typesserver.ts— a typedHandlersinterface,createFetchHandler, validation, and dispatch logic
3. Implement handlers
Import the generated Handlers type and provide your business logic.
The input/output types are already wired — you just fill in the functions:
// server.ts
import { createFetchHandler, type Handlers } from './xrpc/server';
const handlers: Handlers = {
"example.hello": async (params) => ({
message: `Hello, ${params.name}!`,
}),
"example.echo": async (params) => ({
echoed: params.text,
}),
};
const handler = createFetchHandler(handlers); 4. Serve it
Pick your runtime — the handler plugs in directly:
Bun.serve({
port: 3000,
fetch: handler,
}); import express from 'express';
const app = express();
app.use(express.json());
app.post('/api', async (req, res) => {
const request = new Request(`http://localhost${req.url}`, {
method: req.method,
headers: req.headers as HeadersInit,
body: JSON.stringify(req.body),
});
const response = await handler(request);
res.status(response.status).json(await response.json());
});
app.listen(3000); Deno.serve({ port: 3000 }, handler); API Reference
createFetchHandler(handlers, options?)
Creates a Fetch API handler (Request → Response). This is the primary entry point:
const handler = createFetchHandler(handlers, {
validateInputs: true, // Validate inputs with Zod (default: true)
validateOutputs: true, // Validate outputs with Zod (default: true)
getContext: async (request) => ({ /* ... */ }),
formatError: ({ code, message, error }) => ({ code, message }),
}); The handler accepts POST requests with a JSON-RPC-like payload:
{ "method": "example.hello", "params": { "name": "World" } } createRpcHandler(handlers, options?)
Lower-level handler that processes RpcRequest objects directly, without HTTP concerns. Useful for testing or custom transport layers.
Context
Use getContext to extract request context (authentication, logging, etc.) and pass it to all handlers:
type AppContext = { userId: string; role: string };
const handlers: Handlers<AppContext> = {
"example.hello": async (params, ctx) => ({
message: `Hello, ${params.name}! (user: ${ctx.userId})`,
}),
// ...
};
const handler = createFetchHandler(handlers, {
getContext: async (request) => {
const token = request.headers.get('authorization');
return { userId: extractUserId(token), role: 'user' };
},
}); Error Handling
The generated server uses JSON-RPC error codes mapped to HTTP status codes:
| Error Code | HTTP Status | Description |
|---|---|---|
INVALID_REQUEST | 400 | Malformed request |
INVALID_PARAMS | 400 | Input validation failed |
METHOD_NOT_FOUND | 404 | Unknown method |
METHOD_NOT_ALLOWED | 405 | Non-POST request |
INVALID_RESPONSE | 500 | Output validation failed |
INTERNAL_ERROR | 500 | Handler threw an error |
Customize error formatting with formatError:
const handler = createFetchHandler(handlers, {
formatError: ({ code, message, error }) => ({
code,
message,
data: error instanceof ZodError ? error.flatten() : undefined,
}),
}); Type Safety
TypeScript will catch errors if your handlers don't match the contract:
const handlers: Handlers = {
"example.hello": async (params) => ({
// Error: Property 'message' is missing
}),
// Error: Property 'example.echo' is missing
}; Related Target
The TypeScript client is generated separately via the ts-client target (see TypeScript Client).