Skip to content
tRPC

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, createRouterClient and localLink load 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.prefix and loads it on the first request under the prefix. Without a prefix it loads the subtree at startup, unless the handler gets a contract (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
});