Contract-First
Build your first contract-first API with oRPC: describe the API in a contract, implement it with full type checking, and call it from a client that only needs the contract.
In the Getting Started guide, each procedure is defined and implemented in one place, and the client’s types come from the server’s code. Contract-first splits this into two steps. First you write a contract: a description of every procedure, its input, and its output, with no logic inside. Then you implement the contract, and TypeScript checks that the implementation matches it exactly.
This is worth the extra step when:
- You want to agree on the API design before writing code, so frontend and backend work can start at the same time.
- The client lives in another repository or team, and it should depend on the API’s shape, not on server code.
- You want one source of truth the implementation can never drift away from.
This guide follows the same path as Getting Started, in contract-first order:
- Define a contract that describes your API.
- Implement the contract on the server and serve it over HTTP.
- Call it from a fully typed client built from the contract alone.
Installation
Contract-first adds one package to the usual setup: @orpc/contract, which holds the contract builder. As before, you also need a schema library such as Zod, Valibot, ArkType, or any other Standard Schema library.
npm install @orpc/contract@beta @orpc/server@beta @orpc/client@beta zodpnpm add @orpc/contract@beta @orpc/server@beta @orpc/client@beta zodyarn add @orpc/contract@beta @orpc/server@beta @orpc/client@beta zodbun add @orpc/contract@beta @orpc/server@beta @orpc/client@beta zodDefine a Contract
A contract describes a procedure without implementing it. Build one with the oc builder (short for oRPC contract): declare the input with .input, the output with .output, and stop there. A contract has no .handler. Group contracts into a plain object, just like procedures form a router.
import { const oc: ContractBuilder<object>The oRPC contract builder. Chain methods like `.input`, `.errors`, and
`.output` to define procedure contracts, then compose them into router
contracts.oc } from '@orpc/contract'
import * as import zz from 'zod'
const const PlanetSchema: z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>
PlanetSchema = import zz.function object<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}>(shape?: {
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>
object({
id: z.ZodNumberid: import zz.function number(params?: string | z.core.$ZodNumberParams): z.ZodNumbernumber(),
name: z.ZodStringname: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string(),
description: z.ZodOptional<z.ZodString>description: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string().ZodType<any, any, $ZodStringInternals<string>>.optional(): z.ZodOptional<z.ZodString>optional(),
})
export const const listPlanetsContract: ProcedureContractBuilderWithOutput<z.ZodArray<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>, object>
listPlanetsContract = const oc: ContractBuilder<object>The oRPC contract builder. Chain methods like `.input`, `.errors`, and
`.output` to define procedure contracts, then compose them into router
contracts.oc
.ContractBuilder<object>.output<z.ZodArray<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>>(schema: z.ZodArray<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>): ProcedureContractBuilderWithOutput<z.ZodArray<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>, object>
Defines the output schema used to validate and type the procedure output.output(import zz.function array<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>(element: z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, params?: string | z.core.$ZodArrayParams): z.ZodArray<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>
array(const PlanetSchema: z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>
PlanetSchema))
export const const findPlanetContract: ProcedureContractBuilderWithInputOutput<z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, object>
findPlanetContract = const oc: ContractBuilder<object>The oRPC contract builder. Chain methods like `.input`, `.errors`, and
`.output` to define procedure contracts, then compose them into router
contracts.oc
.ContractBuilder<object>.input<z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>>(schema: z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>): ProcedureContractBuilderWithInput<z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, object>
Defines the input schema used to validate and type the procedure input.input(import zz.function object<{
id: z.ZodNumber;
}>(shape?: {
id: z.ZodNumber;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>
object({ id: z.ZodNumberid: import zz.function number(params?: string | z.core.$ZodNumberParams): z.ZodNumbernumber() }))
.ProcedureContractBuilderWithInput<ZodObject<{ id: ZodNumber; }, $strip>, object>.output<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>(schema: z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>): ProcedureContractBuilderWithInputOutput<z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, object>
Defines the output schema used to validate and type the procedure output.output(const PlanetSchema: z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>
PlanetSchema)
export const const createPlanetContract: ProcedureContractBuilderWithInputOutput<z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, object>
createPlanetContract = const oc: ContractBuilder<object>The oRPC contract builder. Chain methods like `.input`, `.errors`, and
`.output` to define procedure contracts, then compose them into router
contracts.oc
.ContractBuilder<object>.input<z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>(schema: z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>): ProcedureContractBuilderWithInput<z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, object>
Defines the input schema used to validate and type the procedure input.input(import zz.function object<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}>(shape?: {
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>
object({ name: z.ZodStringname: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string(), description: z.ZodOptional<z.ZodString>description: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string().ZodType<any, any, $ZodStringInternals<string>>.optional(): z.ZodOptional<z.ZodString>optional() }))
.ProcedureContractBuilderWithInput<ZodObject<{ name: ZodString; description: ZodOptional<ZodString>; }, $strip>, object>.output<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>(schema: z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>): ProcedureContractBuilderWithInputOutput<z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, object>
Defines the output schema used to validate and type the procedure output.output(const PlanetSchema: z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>
PlanetSchema)
export const const contract: {
planet: {
list: ProcedureContractBuilderWithOutput<z.ZodArray<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>, object>;
find: ProcedureContractBuilderWithInputOutput<z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, object>;
create: ProcedureContractBuilderWithInputOutput<z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<...>;
}, z.core.$strip>, z.ZodObject<...>, object>;
};
}
contract = {
planet: {
list: ProcedureContractBuilderWithOutput<z.ZodArray<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>, object>;
find: ProcedureContractBuilderWithInputOutput<z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, object>;
create: ProcedureContractBuilderWithInputOutput<z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<...>;
}, z.core.$strip>, z.ZodObject<...>, object>;
}
planet: {
list: ProcedureContractBuilderWithOutput<z.ZodArray<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>, object>
list: const listPlanetsContract: ProcedureContractBuilderWithOutput<z.ZodArray<z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>, object>
listPlanetsContract,
find: ProcedureContractBuilderWithInputOutput<z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, object>
find: const findPlanetContract: ProcedureContractBuilderWithInputOutput<z.ZodObject<{
id: z.ZodNumber;
}, z.core.$strip>, z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, object>
findPlanetContract,
create: ProcedureContractBuilderWithInputOutput<z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, object>
create: const createPlanetContract: ProcedureContractBuilderWithInputOutput<z.ZodObject<{
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
description: z.ZodOptional<z.ZodString>;
}, z.core.$strip>, object>
createPlanetContract,
},
}
A few things to notice:
.outputmatters here. In Getting Started, the client’s result type flows from the handler’s return value. A contract has no handler, so.outputis where that type comes from. Skip it and the result type becomesunknown..outputis not just a type: once implemented, the server validates every response against it at runtime.- Contracts can also declare typed errors and metadata. Learn more in the Procedure Contract documentation.
Keep this file free of server code. That is what lets the client import it safely later.
Implement the Contract
The implement function turns your contract into a builder that already knows every procedure’s shape. We name it os because it works just like the os builder from Getting Started, except it is locked to your contract.
import { function implement<TContract extends RouterContract, TInitialContext extends Context = DefaultInitialContext>(contract: TContract, config?: ProcedureConfig): Implementer<TContract, TInitialContext & object>Turns a contract into an implementer, used to implement the contract's
procedures, routers, and middleware with full type safety.implement } from '@orpc/server'
const const os: Implementer<{
planet: {
list: ProcedureContractBuilderWithOutput<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<...>;
};
}, DefaultInitialContext & object>
os = implement<{
planet: {
list: ProcedureContractBuilderWithOutput<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<...>;
};
}, DefaultInitialContext>(contract: {
planet: {
list: ProcedureContractBuilderWithOutput<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<...>;
};
}, config?: ProcedureConfig): Implementer<...>
Turns a contract into an implementer, used to implement the contract's
procedures, routers, and middleware with full type safety.implement(const contract: {
planet: {
list: ProcedureContractBuilderWithOutput<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)
export const const listPlanets: ImplementedProcedure<DefaultInitialContext & object, object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>
listPlanets = const os: Implementer<{
planet: {
list: ProcedureContractBuilderWithOutput<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<...>;
};
}, DefaultInitialContext & object>
os.planet: Public<SharedRouterImplementer<{
list: ProcedureContractBuilderWithOutput<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<...>;
}, DefaultInitialContext & object>> & {
...;
}
planet.list: ProcedureImplementer<DefaultInitialContext & object, object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>
list
.ProcedureImplementer<DefaultInitialContext & object, object, InitialInputSchema, ZodArray<ZodObject<{ id: ZodNumber; name: ZodString; description: ZodOptional<...>; }, $strip>>, object>['handler'](handler: ProcedureHandler<DefaultInitialContext & object, unknown, {
id: number;
name: string;
description?: string | undefined;
}[] | AnyORPCError, object>): ImplementedProcedure<DefaultInitialContext & object, object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>
handler(async () => {
// replace with your database query
return [
{ id: numberid: 1, name: stringname: 'Earth' },
{ id: numberid: 2, name: stringname: 'Mars' },
]
})
export const const findPlanet: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>
findPlanet = const os: Implementer<{
planet: {
list: ProcedureContractBuilderWithOutput<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<...>;
};
}, DefaultInitialContext & object>
os.planet: Public<SharedRouterImplementer<{
list: ProcedureContractBuilderWithOutput<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<...>;
}, DefaultInitialContext & object>> & {
...;
}
planet.find: ProcedureImplementer<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>
find
.ProcedureImplementer<DefaultInitialContext & object, object, ZodObject<{ id: ZodNumber; }, $strip>, ZodObject<{ id: ZodNumber; name: ZodString; description: ZodOptional<...>; }, $strip>, object>['handler'](handler: ProcedureHandler<DefaultInitialContext & object, {
id: number;
}, {
id: number;
name: string;
description?: string | undefined;
} | AnyORPCError, object>): ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>
handler(async ({ input: {
id: number;
}
input }) => {
// replace with your database query
return { id: numberid: input: {
id: number;
}
input.id: numberid, name: "Earth"name: 'Earth' }
})
export const const createPlanet: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>
createPlanet = const os: Implementer<{
planet: {
list: ProcedureContractBuilderWithOutput<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<...>;
};
}, DefaultInitialContext & object>
os.planet: Public<SharedRouterImplementer<{
list: ProcedureContractBuilderWithOutput<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<...>;
}, DefaultInitialContext & object>> & {
...;
}
planet.create: ProcedureImplementer<DefaultInitialContext & object, object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>
create
.ProcedureImplementer<DefaultInitialContext & object, object, ZodObject<{ name: ZodString; description: ZodOptional<ZodString>; }, $strip>, ZodObject<...>, object>['handler'](handler: ProcedureHandler<DefaultInitialContext & object, {
name: string;
description?: string | undefined;
}, {
id: number;
name: string;
description?: string | undefined;
} | AnyORPCError, object>): ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>
handler(async ({ input: {
name: string;
description?: string | undefined;
}
input }) => {
// replace with your database insert
return { id: numberid: 3, ...input: {
name: string;
description?: string | undefined;
}
input }
})
export const const router: {
planet: {
list: ImplementedProcedure<DefaultInitialContext & object, object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ImplementedProcedure<...>;
};
}
router = const os: Implementer<{
planet: {
list: ProcedureContractBuilderWithOutput<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<...>;
};
}, DefaultInitialContext & object>
os.router<{
planet: {
list: ImplementedProcedure<DefaultInitialContext & object, object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ImplementedProcedure<...>;
};
}>(router: {
planet: {
list: ImplementedProcedure<DefaultInitialContext & object, object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ImplementedProcedure<...>;
};
}): {
planet: {
list: ImplementedProcedure<DefaultInitialContext & object, object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ImplementedProcedure<...>;
};
}
router({
planet: {
list: ImplementedProcedure<DefaultInitialContext & object, object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ImplementedProcedure<...>;
}
planet: {
list: ImplementedProcedure<DefaultInitialContext & object, object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>
list: const listPlanets: ImplementedProcedure<DefaultInitialContext & object, object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>
listPlanets,
find: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>
find: const findPlanet: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>
findPlanet,
create: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>
create: const createPlanet: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>
createPlanet,
},
})
The contract does the heavy lifting:
os.planet.findalready knows its input and output types, so you only write the logic. Input is still validated before the handler runs.- Return the wrong shape from a handler and TypeScript reports an error immediately.
os.routerchecks completeness: forget to implement a procedure, or put it under the wrong key, and the code does not compile.
Implementations can use middleware and context as usual. Learn more in the Contract Implementation documentation.
Create a Server
Serving the router works exactly as in Getting Started: RPCHandler matches each request to a procedure, validates the input, runs your handler, and sends the result back.
import { function createServer<Request extends typeof IncomingMessage = typeof IncomingMessage, Response extends typeof ServerResponse = typeof ServerResponse>(requestListener?: RequestListener<Request, Response>): Server<Request, Response> (+1 overload)Returns a new instance of
{@link
Server
}
.
The `requestListener` is a function which is automatically
added to the `'request'` event.
```js
import http from 'node:http';
// Create a local server to receive data from
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!',
}));
});
server.listen(8000);
```
```js
import http from 'node:http';
// Create a local server to receive data from
const server = http.createServer();
// Listen to the request event
server.on('request', (request, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!',
}));
});
server.listen(8000);
```createServer } from 'node:http'
import { class RPCHandler<T extends Context>Serves an oRPC router over the RPC protocol using Node.js built-in
HTTP request/response objects.RPCHandler } from '@orpc/server/node'
const const handler: RPCHandler<DefaultInitialContext & object>handler = new new RPCHandler<DefaultInitialContext & object>(router: Router<DefaultInitialContext & object>, options?: NoInfer<RPCHandlerOptions<DefaultInitialContext & object>>): RPCHandler<DefaultInitialContext & object>Serves an oRPC router over the RPC protocol using Node.js built-in
HTTP request/response objects.RPCHandler(const router: {
planet: {
list: ImplementedProcedure<DefaultInitialContext & object, object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ImplementedProcedure<...>;
};
}
router)
const const server: Server<typeof IncomingMessage, typeof ServerResponse>server = createServer<typeof IncomingMessage, typeof ServerResponse>(requestListener?: RequestListener<typeof IncomingMessage, typeof ServerResponse> | undefined): Server<typeof IncomingMessage, typeof ServerResponse> (+1 overload)Returns a new instance of
{@link
Server
}
.
The `requestListener` is a function which is automatically
added to the `'request'` event.
```js
import http from 'node:http';
// Create a local server to receive data from
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!',
}));
});
server.listen(8000);
```
```js
import http from 'node:http';
// Create a local server to receive data from
const server = http.createServer();
// Listen to the request event
server.on('request', (request, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!',
}));
});
server.listen(8000);
```createServer(async (req: IncomingMessagereq, res: ServerResponse<IncomingMessage> & {
req: IncomingMessage;
}
res) => {
const { const matched: booleanmatched } = await const handler: RPCHandler<DefaultInitialContext & object>handler.NodeHttpHandler<DefaultInitialContext & object>.handle(request: NodeHttpRequest, response: NodeHttpResponse, options?: FriendlyStandardHandlerHandleOptions<DefaultInitialContext & object> | undefined): Promise<NodeHttpHandlerHandleResult>handle(req: IncomingMessagereq, res: ServerResponse<IncomingMessage> & {
req: IncomingMessage;
}
res, { prefix?: `/${string}` | undefinedprefix: '/rpc' })
if (const matched: booleanmatched) {
return
}
res: ServerResponse<IncomingMessage> & {
req: IncomingMessage;
}
res.ServerResponse<Request extends IncomingMessage = IncomingMessage>.statusCode: numberWhen using implicit headers (not calling `response.writeHead()` explicitly),
this property controls the status code that will be sent to the client when
the headers get flushed.
```js
response.statusCode = 404;
```
After response header was sent to the client, this property indicates the
status code which was sent out.statusCode = 404
res: ServerResponse<IncomingMessage> & {
req: IncomingMessage;
}
res.Stream.Writable.end(chunk: any, cb?: () => void): ServerResponse<IncomingMessage> & {
req: IncomingMessage;
} (+2 overloads)
Signals that no more data will be written, with one final chunk of data.end('Not found')
})
const server: Server<typeof IncomingMessage, typeof ServerResponse>server.Server.listen(port?: number, hostname?: string, listeningListener?: (() => void) | undefined): Server<typeof IncomingMessage, typeof ServerResponse> (+8 overloads)Start a server listening for connections. A `net.Server` can be a TCP or
an `IPC` server depending on what it listens to.
Possible signatures:
* `server.listen(handle[, backlog][, callback])`
* `server.listen(options[, callback])`
* `server.listen(path[, backlog][, callback])` for `IPC` servers
* `server.listen([port[, host[, backlog]]][, callback])` for TCP servers
This function is asynchronous. When the server starts listening, the `'listening'` event will be emitted. The last parameter `callback`will be added as a listener for the `'listening'`
event.
All `listen()` methods can take a `backlog` parameter to specify the maximum
length of the queue of pending connections. The actual length will be determined
by the OS through sysctl settings such as `tcp_max_syn_backlog` and `somaxconn` on Linux. The default value of this parameter is 511 (not 512).
All
{@link
Socket
}
are set to `SO_REUSEADDR` (see [`socket(7)`](https://man7.org/linux/man-pages/man7/socket.7.html) for
details).
The `server.listen()` method can be called again if and only if there was an
error during the first `server.listen()` call or `server.close()` has been
called. Otherwise, an `ERR_SERVER_ALREADY_LISTEN` error will be thrown.
One of the most common errors raised when listening is `EADDRINUSE`.
This happens when another server is already listening on the requested`port`/`path`/`handle`. One way to handle this would be to retry
after a certain amount of time:
```js
server.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
console.error('Address in use, retrying...');
setTimeout(() => {
server.close();
server.listen(PORT, HOST);
}, 1000);
}
});
```listen(3000, '127.0.0.1', () => var console: Consoleconsole.Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log('Listening on 127.0.0.1:3000'))
Create a Client
Here is the payoff. Type the client with RouterContractClient<typeof contract>: it needs only the contract, which contains no business logic, so no server code can leak into the client bundle.
import type { type RouterContractClient<TRouter extends RouterContract, TClientContext extends ClientContext = object> = TRouter extends ProcedureContract<infer UInputSchema extends AnySchema, infer UOutputSchema extends AnySchema, infer UErrorMap extends ErrorMap> ? ProcedureContractClient<TClientContext, UInputSchema, UOutputSchema, UErrorMap> : { [K in keyof TRouter]: TRouter[K] extends RouterContract ? RouterContractClient<TRouter[K], TClientContext> : never; }Client type inferred from a router contract, preserving its shape.
Useful for typing a client without importing the server router.RouterContractClient } from '@orpc/contract'
import { function createORPCClient<T extends AnyNestedClient>(link: ClientLink<InferClientContext<T>>, { path, ...options }?: NoInfer<ORPCClientOptions<T>>): TCreates a fully typed oRPC client from a link.
The returned client mirrors the shape of your router or contract,
so calling a procedure is as simple as calling a function.createORPCClient } from '@orpc/client'
import { class RPCLink<T extends ClientContext>Client link that communicates with an RPC Handler over the Fetch API (HTTP).RPCLink } from '@orpc/client/fetch'
const const link: RPCLink<ClientContext>link = new new RPCLink<ClientContext>(options: RPCLinkOptions<ClientContext>): RPCLink<ClientContext>Client link that communicates with an RPC Handler over the Fetch API (HTTP).RPCLink({
FetchLinkTransportOptions<ClientContext>.origin?: Value<Promisable<`https://${string}` | `http://${string}` | ({} & string) | undefined>, [options: ClientOptions<ClientContext>, path: string[]]>The origin to prepend to all request URLs, useful for CORS requests.origin: 'http://127.0.0.1:3000',
RPCLinkCodecOptions<ClientContext>.url?: Value<Promisable<StandardUrl>, [options: ClientOptions<ClientContext>, path: string[], input: unknown]> | undefinedBase url for all requests (without origin). Should match with handler's prefix.url: '/rpc', // <- must match the server's prefix
})
export const const orpc: {
planet: {
list: ProcedureContractClient<object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractClient<...>;
};
}
orpc: type RouterContractClient<TRouter extends RouterContract, TClientContext extends ClientContext = object> = TRouter extends ProcedureContract<infer UInputSchema extends AnySchema, infer UOutputSchema extends AnySchema, infer UErrorMap extends ErrorMap> ? ProcedureContractClient<TClientContext, UInputSchema, UOutputSchema, UErrorMap> : { [K in keyof TRouter]: TRouter[K] extends RouterContract ? RouterContractClient<TRouter[K], TClientContext> : never; }Client type inferred from a router contract, preserving its shape.
Useful for typing a client without importing the server router.RouterContractClient<typeof const contract: {
planet: {
list: ProcedureContractBuilderWithOutput<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> = createORPCClient<{
planet: {
list: ProcedureContractClient<object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractClient<...>;
};
}>(link: ClientLink<...>, { path, ...options }?: NoInfer<ORPCClientOptions<...>>): {
planet: {
list: ProcedureContractClient<object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractClient<...>;
};
}
Creates a fully typed oRPC client from a link.
The returned client mirrors the shape of your router or contract,
so calling a procedure is as simple as calling a function.createORPCClient(const link: RPCLink<ClientContext>link)
Call a Procedure
Calling procedures feels exactly the same as in Getting Started:
const const planets: {
id: number;
name: string;
description?: string | undefined;
}[]
planets = await const orpc: {
planet: {
list: ProcedureContractClient<object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractClient<...>;
};
}
orpc.planet: {
list: ProcedureContractClient<object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractClient<...>;
}
planet.list: Client
(input?: void | undefined, options?: FriendlyClientOptions<object> | undefined) => PromiseWithError<{
id: number;
name: string;
description?: string | undefined;
}[], Error>
list()
const const planet: {
id: number;
name: string;
description?: string | undefined;
}
planet = await const orpc: {
planet: {
list: ProcedureContractClient<object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractClient<...>;
};
}
orpc.planet: {
list: ProcedureContractClient<object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractClient<...>;
}
planet.find: Client
(input: {
id: number;
}, options?: FriendlyClientOptions<object> | undefined) => PromiseWithError<{
id: number;
name: string;
description?: string | undefined;
}, Error>
find({ id: numberid: 1 })
const orpc: {
planet: {
list: ProcedureContractClient<object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractClient<...>;
};
}
orpc.planet: {
list: ProcedureContractClient<object, InitialInputSchema, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ProcedureContractClient<object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>;
create: ProcedureContractClient<...>;
}
planet.- create
- find
- list
create: ProcedureContractClient<object, ZodObject<{
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>, object>
create
//
//
Both sides now answer to the contract. The server cannot ship a response the contract does not allow, the client cannot send input the contract rejects, and changing the contract surfaces every affected handler and call site as a compile error.
Next Steps
- Learn contracts in depth: Procedure Contract and Router Contract
- Add middleware and context to implementations in Contract Implementation
- Already have an OpenAPI spec? Generate a contract from it
- Keep type checking fast in large codebases with the Contract Client Factory
- Expose the contract as a REST API with the OpenAPI Handler and share it via an OpenAPI specification