Implement Contract in NestJS
This guide explains how to easily implement oRPC contract within your NestJS application using @orpc/nest
.
WARNING
This feature is currently experimental and may be subject to breaking changes.
Installation
npm install @orpc/nest@latest
yarn add @orpc/nest@latest
pnpm add @orpc/nest@latest
bun add @orpc/nest@latest
deno install npm:@orpc/nest@latest
Requirements
oRPC is an ESM-only library. Therefore, your NestJS application must be configured to support ESM modules.
Configure
tsconfig.json
: with"module": "NodeNext"
or a similar ESM-compatible option.json{ "compilerOptions": { "module": "NodeNext", // <-- this is recommended "strict": true // <-- this is recommended // ... other options, } }
Node.js Environment:
- Node.js 22+: Recommended, as it allows
require()
of ESM modules natively. - Older Node.js versions: Alternatively, use a bundler to compile ESM modules (including
@orpc/nest
) to CommonJS.
- Node.js 22+: Recommended, as it allows
Define Your Contract
Before implementation, define your oRPC contract. This process is consistent with the standard oRPC methodology. For detailed guidance, refer to the main Contract-First guide.
Example Contract
import { populateContractRouterPaths } from '@orpc/nest'
import { oc } from '@orpc/contract'
import { z } from 'zod'
export const PlanetSchema = z.object({
id: z.number().int().min(1),
name: z.string(),
description: z.string().optional(),
})
export const listPlanetContract = oc
.route({
method: 'GET',
path: '/planets' // Path is required for NestJS implementation
})
.input(
z.object({
limit: z.number().int().min(1).max(100).optional(),
cursor: z.number().int().min(0).default(0),
}),
)
.output(z.array(PlanetSchema))
export const findPlanetContract = oc
.route({
method: 'GET',
path: '/planets/{id}' // Path is required
})
.input(PlanetSchema.pick({ id: true }))
.output(PlanetSchema)
export const createPlanetContract = oc
.route({
method: 'POST',
path: '/planets' // Path is required
})
.input(PlanetSchema.omit({ id: true }))
.output(PlanetSchema)
/**
* populateContractRouterPaths is completely optional,
* because the procedure's path is required for NestJS implementation.
* This utility automatically populates any missing paths
* Using the router's keys + `/`.
*/
export const contract = populateContractRouterPaths({
planet: {
list: listPlanetContract,
find: findPlanetContract,
create: createPlanetContract,
},
})
WARNING
For a contract to be implementable in NestJS using @orpc/nest
, each contract must define a path
in its .route
. Omitting it will cause a build‑time error. You can avoid this by using the populateContractRouterPaths
utility to automatically fill in any missing paths.
Path Parameters
Aside from oRPC Path Parameters, regular NestJS route patterns still work out of the box. However, they are not standard in OpenAPI, so we recommend using oRPC Path Parameters exclusively.
WARNING
oRPC Path Parameter matching with slashes (/) does not work on the NestJS Fastify platform, because Fastify does not allow wildcard (*
) aliasing in path parameters.
Implement Your Contract
import { Implement, implement, ORPCError } from '@orpc/nest'
@Controller()
export class PlanetController {
/**
* Implement a standalone procedure
*/
@Implement(contract.planet.list)
list() {
return implement(contract.planet.list).handler(({ input }) => {
// Implement logic here
return []
})
}
/**
* Implement entire a contract
*/
@Implement(contract.planet)
planet() {
return {
list: implement(contract.planet.list).handler(({ input }) => {
// Implement logic here
return []
}),
find: implement(contract.planet.find).handler(({ input }) => {
// Implement logic here
return {
id: 1,
name: 'Earth',
description: 'The planet Earth',
}
}),
create: implement(contract.planet.create).handler(({ input }) => {
// Implement logic here
return {
id: 1,
name: 'Earth',
description: 'The planet Earth',
}
}),
}
}
// other handlers...
}
INFO
The @Implement
decorator functions similarly to NestJS built-in HTTP method decorators (e.g., @Get
, @Post
). Handlers decorated with @Implement
are standard NestJS controller handlers and can leverage all NestJS features.
Body Parser
By default, NestJS parses request bodies for application/json
and application/x-www-form-urlencoded
content types. However:
- NestJS
urlencoded
parser does not support Bracket Notation like in standard oRPC parsers. - In some edge cases like uploading a file with
application/json
content type, the NestJS parser does not treat it as a file, instead it parses the body as a JSON string.
Therefore, we recommend disabling the NestJS body parser:
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bodyParser: false,
})
await app.listen(process.env.PORT ?? 3000)
}
INFO
oRPC will use NestJS parsed body when it's available, and only use the oRPC parser if the body is not parsed by NestJS.
Create a Type-Safe Client
When you implement oRPC contracts in NestJS using @orpc/nest
, the resulting API endpoints are OpenAPI compatible. This allows you to use an OpenAPI-compatible client link, such as OpenAPILink, to interact with your API in a type-safe way.
import type { JsonifiedClient } from '@orpc/openapi-client'
import type { ContractRouterClient } from '@orpc/contract'
import { createORPCClient } from '@orpc/client'
import { OpenAPILink } from '@orpc/openapi-client/fetch'
const link = new OpenAPILink(contract, {
url: 'http://localhost:3000',
headers: () => ({
'x-api-key': 'my-api-key',
}),
// fetch: <-- polyfill fetch if needed
})
const client: JsonifiedClient<ContractRouterClient<typeof contract>> = createORPCClient(link)
INFO
Please refer to the OpenAPILink documentation for more information on client setup and options.