API Contract Definition
This guide shows how to define your xRPC API contract using TypeScript and Zod schemas. The API contract is a pure DSL - it defines schemas and endpoints only, with no implementation details. Handlers are implemented separately in server files.
Important: Your contract file must export router:
export const router = createRouter({ ... });
Core Concepts
An xRPC API contract uses a hierarchical structure:
- Router: Primary grouping mechanism that exports all endpoints (single export per contract)
- Endpoint Groups: Named API namespaces (like
greeting,user,product) - Queries & Mutations: Individual RPC methods within a group
- Types: Shared data schemas defined with Zod (like protobuf messages)
The hierarchy is: Router → Endpoint Groups → Queries/Mutations
Simple Example
import { z } from 'zod';
import { createRouter, group, query, mutation } from 'xrpckit';
// Types: Shared data schemas
const GreetingInput = z.object({ name: z.string() });
const GreetingOutput = z.object({ message: z.string() });
// Endpoint group: Named API namespace
const greeting = group("greeting", {
// Query: Read operation
greet: query({
input: GreetingInput,
output: GreetingOutput,
}),
// Mutation: Write operation
setGreeting: mutation({
input: z.object({ name: z.string(), greeting: z.string() }),
output: GreetingOutput,
}),
});
// Router: Primary grouping mechanism (single export)
export const router = createRouter({
greeting, // Endpoint group
});
Multiple Endpoints
import { z } from 'zod';
import { createRouter, group, query, mutation } from 'xrpckit';
// User group
const user = group("user", {
getUser: query({
input: z.object({ id: z.string() }),
output: z.object({ id: z.string(), name: z.string(), email: z.string() }),
}),
updateUser: mutation({
input: z.object({ id: z.string(), name: z.string().optional(), email: z.string().optional() }),
output: z.object({ id: z.string(), name: z.string(), email: z.string() }),
}),
});
// Product group
const product = group("product", {
listProducts: query({
input: z.object({ limit: z.number().optional(), offset: z.number().optional() }),
output: z.array(z.object({ id: z.string(), name: z.string(), price: z.number() })),
}),
createProduct: mutation({
input: z.object({ name: z.string(), price: z.number() }),
output: z.object({ id: z.string(), name: z.string(), price: z.number() }),
}),
});
// Router: Primary grouping mechanism (single export)
export const router = createRouter({
user, // Endpoint group
product, // Endpoint group
});
Inline Types
For simple cases, define types inline:
import { z } from 'zod';
import { createRouter, group, query, mutation } from 'xrpckit';
export const router = createRouter({
user: group("user", {
getUser: query({
input: z.object({ id: z.string() }),
output: z.object({ id: z.string(), name: z.string() }),
}),
updateUser: mutation({
input: z.object({ id: z.string(), name: z.string() }),
output: z.object({ id: z.string(), name: z.string() }),
}),
}),
});
Shared Types
For reusable types, define them separately:
import { z } from 'zod';
import { createRouter, group, query, mutation } from 'xrpckit';
// Shared types
const UserId = z.string();
const User = z.object({ id: UserId, name: z.string(), email: z.string() });
const UserUpdate = z.object({ name: z.string().optional(), email: z.string().optional() });
export const router = createRouter({
user: group("user", {
getUser: query({
input: z.object({ id: UserId }),
output: User,
}),
updateUser: mutation({
input: z.object({ id: UserId }).merge(UserUpdate),
output: User,
}),
}),
});
Query vs Mutation
Each method within a group is either a query or mutation:
- Query: Read operations that don’t modify state
- Mutation: Write operations that modify state
Both use the same structure with input and output properties. No handlers in the contract - those are implemented separately in server files (see Go Server, TypeScript Server, or Kotlin Server).
Flat Endpoints (No Groups)
For small contracts, you can define endpoints directly at the router root:
import { z } from 'zod';
import { createRouter, query, mutation } from 'xrpckit';
export const router = createRouter({
ping: query({
input: z.object({}),
output: z.object({ ok: z.boolean() }),
}),
setStatus: mutation({
input: z.object({ status: z.string() }),
output: z.object({ success: z.boolean() }),
}),
});
This produces method names like ping and setStatus (without a group prefix).
Code Generation
After defining your contract, generate code for your target frameworks:
# Generate for specific framework targets
xrpc generate --targets go-server,ts-client
Planned targets: go-client is not yet available in the CLI.
Note: The xRPC CLI runs on Node.js (>= 18). Generated code runs on native runtimes for each target language (Go runtime, Node.js/Bun, etc.).