Router Contract
Router contracts define the shape of a router without including any business logic. Use them for documentation, testing, and keeping multiple implementations of the same router aligned.
Overview
Define a router contract as a plain JavaScript object where each key maps to a procedure contract:
import { const oc: ContractBuilder<object>The contract builder — the entry point for defining procedure and router contracts
(input/output schemas, errors, and metadata) without any business logic.oc } from '@orpc/contract'
const const ping: ProcedureContractBuilderWithOutput<z.ZodString, object>ping = const oc: ContractBuilder<object>The contract builder — the entry point for defining procedure and router contracts
(input/output schemas, errors, and metadata) without any business logic.oc.ContractBuilder<object>.output<z.ZodString>(schema: z.ZodString): ProcedureContractBuilderWithOutput<z.ZodString, object>output(import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string())
const const pong: ProcedureContractBuilderWithOutput<z.ZodString, object>pong = const oc: ContractBuilder<object>The contract builder — the entry point for defining procedure and router contracts
(input/output schemas, errors, and metadata) without any business logic.oc.ContractBuilder<object>.output<z.ZodString>(schema: z.ZodString): ProcedureContractBuilderWithOutput<z.ZodString, object>output(import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string())
export const const router: {
ping: ProcedureContractBuilderWithOutput<z.ZodString, object>;
pong: ProcedureContractBuilderWithOutput<z.ZodString, object>;
nested: {
ping: ProcedureContractBuilderWithOutput<z.ZodString, object>;
pong: ProcedureContractBuilderWithOutput<z.ZodString, object>;
};
}
router = {
ping: ProcedureContractBuilderWithOutput<z.ZodString, object>ping,
pong: ProcedureContractBuilderWithOutput<z.ZodString, object>pong,
nested: {
ping: ProcedureContractBuilderWithOutput<z.ZodString, object>;
pong: ProcedureContractBuilderWithOutput<z.ZodString, object>;
}
nested: { ping: ProcedureContractBuilderWithOutput<z.ZodString, object>ping, pong: ProcedureContractBuilderWithOutput<z.ZodString, object>pong }
}
Extending Router
You can extend a router contract with shared configuration, such as attaching metadata to every procedure:
const router = oc.meta(requireAuthMeta).router({
ping,
pong,
nested: {
ping,
pong,
}
})
Router to Contract
A normal router can be used as a contract router as long as it does not include a lazy router. If necessary, use unlazyRouter to fully resolve it and make it contract-compatible.
import { unlazyRouter } from '@orpc/server'
const compatibleContract = await unlazyRouter(router)
Safely Importing Router on the Client
Sometimes you need to import the contract on the client, for example when using OpenAPI Link. If you derive the contract from a router, importing it directly can be heavy and may expose internal logic. To avoid this, follow the steps below to safely minify and export the contract.
-
Minify the Contract Router and Export to JSON
import fs from 'node:fs' import { unlazyRouter } from '@orpc/server' import { minifyRouterContract } from '@orpc/contract' const compatibleContract = await unlazyRouter(router) const minifiedRouter = minifyRouterContract(compatibleContract) fs.writeFileSync('./contract.json', JSON.stringify(minifiedRouter))::: info
minifyRouterContractpreserves only the metadata needed by the client; all other data is stripped out. ::: -
Import the Contract JSON on the Client Side
import contract from './contract.json' const link = new OpenAPILink(contract as typeof router)::: info Cast
contracttotypeof routerto preserve type safety, since standard schema types cannot be serialized to JSON and must be cast manually. :::
Utilities
Infer Router Contract Inputs
Infers the input type of each procedure contract in a router contract.
import type { type InferRouterContractInputs<T extends RouterContract> = T extends ProcedureContract<infer UInputSchema extends AnySchema, any, any> ? InferSchemaInput<UInputSchema> : { [K in keyof T]: T[K] extends RouterContract ? InferRouterContractInputs<T[K]> : never; }Infer the input types for each procedure-contract, preserving the router-contract shape.InferRouterContractInputs } from '@orpc/contract'
export type type Inputs = {
planet: {
list: {
limit?: number | undefined;
cursor?: number | undefined;
};
find: {
id: number;
};
create: {
name: string;
description?: string | undefined;
};
};
}
Inputs = type InferRouterContractInputs<T extends RouterContract> = T extends ProcedureContract<infer UInputSchema extends AnySchema, any, any> ? InferSchemaInput<UInputSchema> : { [K in keyof T]: T[K] extends RouterContract ? InferRouterContractInputs<T[K]> : never; }Infer the input types for each procedure-contract, preserving the router-contract shape.InferRouterContractInputs<typeof const contract: {
planet: {
list: ProcedureContractBuilderWithInputOutput<ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractBuilderWithInputOutput<ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractBuilderWithInputOutput<...>;
};
}
contract>
type type FindPlanetInput = {
id: number;
}
FindPlanetInput = type Inputs = {
planet: {
list: {
limit?: number | undefined;
cursor?: number | undefined;
};
find: {
id: number;
};
create: {
name: string;
description?: string | undefined;
};
};
}
Inputs['planet']['find']
Infer Router Contract Outputs
Infers the output type of each procedure contract in a router contract.
import type { type InferRouterContractOutputs<T extends RouterContract> = T extends ProcedureContract<any, infer UOutputSchema extends AnySchema, any> ? InferSchemaOutput<UOutputSchema> : { [K in keyof T]: T[K] extends RouterContract ? InferRouterContractOutputs<T[K]> : never; }Infer the output types for each procedure-contract, preserving the router-contract shape.InferRouterContractOutputs } from '@orpc/contract'
export type type Outputs = {
planet: {
list: {
id: number;
name: string;
description?: string | undefined;
}[];
find: {
id: number;
name: string;
description?: string | undefined;
};
create: {
id: number;
name: string;
description?: string | undefined;
};
};
}
Outputs = type InferRouterContractOutputs<T extends RouterContract> = T extends ProcedureContract<any, infer UOutputSchema extends AnySchema, any> ? InferSchemaOutput<UOutputSchema> : { [K in keyof T]: T[K] extends RouterContract ? InferRouterContractOutputs<T[K]> : never; }Infer the output types for each procedure-contract, preserving the router-contract shape.InferRouterContractOutputs<typeof const contract: {
planet: {
list: ProcedureContractBuilderWithInputOutput<ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractBuilderWithInputOutput<ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractBuilderWithInputOutput<...>;
};
}
contract>
type type FindPlanetOutput = {
id: number;
name: string;
description?: string | undefined;
}
FindPlanetOutput = type Outputs = {
planet: {
list: {
id: number;
name: string;
description?: string | undefined;
}[];
find: {
id: number;
name: string;
description?: string | undefined;
};
create: {
id: number;
name: string;
description?: string | undefined;
};
};
}
Outputs['planet']['find']
Infer Router Contract Error Map
Collects the error maps from every procedure contract in a router contract into a single type.
import type { type InferRouterContractErrorMap<T extends RouterContract> = T extends ProcedureContract<any, any, infer UErrorMap extends ErrorMap> ? UErrorMap : { [K in keyof T]: T[K] extends RouterContract ? InferRouterContractErrorMap<T[K]> : never; }[keyof T]Infer the union of error maps defined across the entire router-contract.InferRouterContractErrorMap } from '@orpc/contract'
export type type ErrorMap = objectErrorMap = type InferRouterContractErrorMap<T extends RouterContract> = T extends ProcedureContract<any, any, infer UErrorMap extends ErrorMap> ? UErrorMap : { [K in keyof T]: T[K] extends RouterContract ? InferRouterContractErrorMap<T[K]> : never; }[keyof T]Infer the union of error maps defined across the entire router-contract.InferRouterContractErrorMap<typeof const contract: {
planet: {
list: ProcedureContractBuilderWithInputOutput<ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractBuilderWithInputOutput<ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractBuilderWithInputOutput<...>;
};
}
contract>
Infer Router Contract Errors
Infers the throwable errors each procedure contract in a router contract can describe.
import type { type InferRouterContractErrors<T extends RouterContract> = T extends ProcedureContract<any, any, infer UErrorMap extends ErrorMap> ? ORPCErrorFromErrorMap<UErrorMap> | Error : { [K in keyof T]: T[K] extends RouterContract ? InferRouterContractErrors<T[K]> : never; }Infer throwable errors for each procedure-contract, preserving the router-contract shape.InferRouterContractErrors } from '@orpc/contract'
export type type Errors = {
planet: {
list: Error;
find: Error;
create: Error;
};
}
Errors = type InferRouterContractErrors<T extends RouterContract> = T extends ProcedureContract<any, any, infer UErrorMap extends ErrorMap> ? ORPCErrorFromErrorMap<UErrorMap> | Error : { [K in keyof T]: T[K] extends RouterContract ? InferRouterContractErrors<T[K]> : never; }Infer throwable errors for each procedure-contract, preserving the router-contract shape.InferRouterContractErrors<typeof const contract: {
planet: {
list: ProcedureContractBuilderWithInputOutput<ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractBuilderWithInputOutput<ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractBuilderWithInputOutput<...>;
};
}
contract>
type type FindPlanetError = ErrorFindPlanetError = type Errors = {
planet: {
list: Error;
find: Error;
create: Error;
};
}
Errors['planet']['find']
Infer Router Contract Error
Infers all possible throwable errors the entire router contract can describe. This is useful when you want a single type for contract-wide error handling.
import type { type InferRouterContractError<T extends RouterContract> = T extends ProcedureContract<any, any, infer UErrorMap extends ErrorMap> ? ORPCErrorFromErrorMap<UErrorMap> | Error : { [K in keyof T]: T[K] extends RouterContract ? InferRouterContractError<T[K]> : never; }[keyof T]Infer the union of throwable errors for entire router-contract.InferRouterContractError } from '@orpc/contract'
export type type ContractError = ErrorContractError = type InferRouterContractError<T extends RouterContract> = T extends ProcedureContract<any, any, infer UErrorMap extends ErrorMap> ? ORPCErrorFromErrorMap<UErrorMap> | Error : { [K in keyof T]: T[K] extends RouterContract ? InferRouterContractError<T[K]> : never; }[keyof T]Infer the union of throwable errors for entire router-contract.InferRouterContractError<typeof const contract: {
planet: {
list: ProcedureContractBuilderWithInputOutput<ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractBuilderWithInputOutput<ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractBuilderWithInputOutput<...>;
};
}
contract>