Routers
Plain objects of procedures, merged with mergeRouters and split with lazy().
On this page
A router is a plain object. Keys are path segments: { post: { byId } } serves post.byId, at /trpc/post.byId over HTTP.
export const const appRouter: {
health: BuildProcedure<RootDef<Context, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
post: {
byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
id: ZodString;
}, $strip>>, "query", {
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}>;
services: never;
unmapped: never;
}>;
create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
ctx: Context;
ctxAdded: {
...;
};
inputOut: Unset;
errors: TrpcError<...>;
shorts: never;
}>, ZodObject<...>>, "mutation", {
...;
}>;
};
}
appRouter = {
health: BuildProcedure<RootDef<Context, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>
health: const t: TrpcRoot<Context, {}, RootDef<Context, {}>>t.TrpcRoot<Context, {}, RootDef<Context, {}>>.procedure: ProcedureBuilder<RootDef<Context, {}>>procedure.ProcedureBuilder<RootDef<Context, {}>>.query<string>(resolver: (opts: ResolverOpts<RootDef<Context, {}>>) => string): BuildProcedure<RootDef<Context, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>
query(() => 'ok'),
post: {
byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
id: ZodString;
}, $strip>>, "query", {
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}>;
services: never;
unmapped: never;
}>;
create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
ctx: Context;
ctxAdded: {
...;
};
inputOut: Unset;
errors: TrpcError<...>;
shorts: never;
}>, ZodObject<...>>, "mutation", {
...;
}>;
}
post: { byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
id: ZodString;
}, $strip>>, "query", {
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}>;
services: never;
unmapped: never;
}>
byId, create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
ctx: Context;
ctxAdded: {
...;
};
inputOut: Unset;
errors: TrpcError<...>;
shorts: never;
}>, ZodObject<...>>, "mutation", {
...;
}>
create },
};Merge routers with mergeRouters(a, b). A key in both is a type error and a runtime error; object spread would silently keep the last.
For OpenAPI, t.router({ meta }, routes) groups routes under a prefix and tags without changing their types or RPC paths (Groups).
Lazy subtrees
lazy() loads a subtree on its first call, for large routers that should start fast (serverless cold starts). The client types it as if it were inline.
import { function lazy<TRouter extends AnyRouter>(load: () => Promise<TRouter | {
default: TRouter;
}>): LazyRouter<TRouter>
lazy } from '@trpc12/trpc/server';
export const const appRouter: {
post: {
byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
id: ZodString;
}, $strip>>, "query", {
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}>;
services: never;
unmapped: never;
}>;
list: BuildProcedure<RootDef<Context, {}>, "query", {
output: Post[];
errors: never;
services: never;
unmapped: never;
}>;
create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
ctx: Context;
ctxAdded: {
...;
};
inputOut: Unset;
errors: TrpcError<...>;
shorts: never;
}>, ZodObject<...>>, "mutation", {
...;
}>;
};
admin: LazyRouter<...>;
}
appRouter = {
post: {
byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
id: ZodString;
}, $strip>>, "query", {
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}>;
services: never;
unmapped: never;
}>;
list: BuildProcedure<RootDef<Context, {}>, "query", {
output: Post[];
errors: never;
services: never;
unmapped: never;
}>;
create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
ctx: Context;
ctxAdded: {
...;
};
inputOut: Unset;
errors: TrpcError<...>;
shorts: never;
}>, ZodObject<...>>, "mutation", {
...;
}>;
}
post: const postRouter: {
byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
id: ZodString;
}, $strip>>, "query", {
output: Post;
errors: TrpcError<"NOT_FOUND", {
id: string;
}>;
services: never;
unmapped: never;
}>;
list: BuildProcedure<RootDef<Context, {}>, "query", {
output: Post[];
errors: never;
services: never;
unmapped: never;
}>;
create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
ctx: Context;
ctxAdded: {
...;
};
inputOut: Unset;
errors: TrpcError<...>;
shorts: never;
}>, ZodObject<...>>, "mutation", {
...;
}>;
}
postRouter,
admin: LazyRouter<{
stats: BuildProcedure<RootDef<Context, {}>, "query", {
output: number;
errors: never;
services: never;
unmapped: never;
}>;
}>
admin: lazy<{
stats: BuildProcedure<RootDef<Context, {}>, "query", {
output: number;
errors: never;
services: never;
unmapped: never;
}>;
}>(load: () => Promise<{
stats: BuildProcedure<RootDef<Context, {}>, "query", {
output: number;
errors: never;
services: never;
unmapped: never;
}>;
} | {
default: {
stats: BuildProcedure<RootDef<Context, {}>, "query", {
output: number;
errors: never;
services: never;
unmapped: never;
}>;
};
}>): LazyRouter<{
stats: BuildProcedure<RootDef<Context, {}>, "query", {
output: number;
errors: never;
services: never;
unmapped: never;
}>;
}>
lazy(() => import('./admin.ts')),
};t.lazy(load, { meta }) also takes router-level meta, as t.router() does: t.lazy(() => import('./admin.ts'), { meta: { route: { prefix: '/admin' } } }).
load may return the router or a module whose default export is the router. Each subtree loads once.
- The RPC handlers,
createRouterClientandlocalLinkload it on the first call under it. - Tools that need the whole router ask for it loaded:
generateOpenApi(await loadRouter(appRouter), …),toContract(await loadRouter(appRouter)). - The OpenAPI handler finds a lazy subtree's routes by its
route.prefixand loads it on the first request under the prefix. Without a prefix it loads the subtree at startup, unless the handler gets acontract(below).
With route meta, the subtree is scoped like a group, so all its OpenAPI routes start with the prefix.
To load nothing until it's called, generate the contract at build time and pass it to the OpenAPI handler:
// build step
await module "node:fs/promises"fs.function writeFile(file: PathLike | fs.FileHandle, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView>, options?: (ObjectEncodingOptions & {
mode?: Mode | undefined;
flag?: OpenMode | undefined;
flush?: boolean | undefined;
} & EventEmitter<T extends EventMap<T> = any>.Abortable) | BufferEncoding | null): Promise<void>
writeFile(
'contract.json',
var JSON: JSONJSON.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)stringify(toContract<{
health: BuildProcedure<RootDef<{}, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
}>(router: {
health: BuildProcedure<RootDef<{}, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
}): ContractJson<{
health: BuildProcedure<RootDef<{}, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
}>
toContract(await loadRouter<{
health: BuildProcedure<RootDef<{}, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
}>(router: {
health: BuildProcedure<RootDef<{}, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
}): Promise<{
health: BuildProcedure<RootDef<{}, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
}>
loadRouter(const appRouter: {
health: BuildProcedure<RootDef<{}, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
}
appRouter))),
);
// server
createOpenApiHandler<{
health: BuildProcedure<RootDef<{}, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
}, undefined>(opts: OpenApiHandlerOptions<{
health: BuildProcedure<RootDef<{}, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
}, undefined>): OpenApiHandler<undefined>
createOpenApiHandler({ BaseOpenApiHandlerOptions<{ health: BuildProcedure<RootDef<{}, {}>, "query", { output: string; errors: never; services: never; unmapped: never; }>; }>.router: {
health: BuildProcedure<RootDef<{}, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
}
router: const appRouter: {
health: BuildProcedure<RootDef<{}, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
}
appRouter, BaseOpenApiHandlerOptions<{ health: BuildProcedure<RootDef<{}, {}>, "query", { output: string; errors: never; services: never; unmapped: never; }>; }>.contract?: ContractJson<{
health: BuildProcedure<RootDef<{}, {}>, "query", {
output: string;
errors: never;
services: never;
unmapped: never;
}>;
}> | undefined
contract });