Implement oRPC contract with NestJS
oRPC provides built-in support for NestJS applications through the @orpc/nest package. It lets you implement oRPC contracts in NestJS while maintaining type safety and OpenAPI compatibility.
Installation
npm install @orpc/nest@betayarn add @orpc/nest@betapnpm add @orpc/nest@betabun add @orpc/nest@betadeno add npm:@orpc/nest@betaRequirements
oRPC is an ESM-only library, but NestJS versions below v12 do not natively support ESM. You might need to configure your project for ESM and use a Node.js version that supports require() for ESM modules (Node.js 22+ is recommended). The following configuration is recommended:
{
"compilerOptions": {
"module": "NodeNext", // <- recommended
"strict": true // <- recommended
// ... other options
}
}Define Your Contract
Before implementation, define your contract as usual, including routing. There is no special requirement, except that each contract must define an openapi.path meta.
import { oc } from '@orpc/contract'
import { openapi, populateRouterContractOpenAPIPaths } from '@orpc/openapi'
const example = oc.meta(openapi({
path: '/example'
}))
// or using the `populateRouterContractOpenAPIPaths` helper to
// automatically populate OpenAPI paths for all contracts
const contract = populateRouterContractOpenAPIPaths({
example
})Implement Your Contract
To implement your contract in NestJS, use the @Implement decorator and the implement function. The @Implement very similar to NestJS built-in HTTP method decorators (e.g., @Get, @Post) and can be used to implement either a single procedure contract or an router contract or combine with other NestJS decorators.
import { Implement } from '@orpc/nest'
import { implement, ORPCError } from '@orpc/server'
@Controller()
export class PlanetController {
/**
* Implement a procedure contract
*/
@Implement(contract.planet.list)
list() {
return implement(contract.planet.list).handler(({ input }) => {
// Implement logic here
})
}
/**
* Implement a router contract
*/
@Implement(contract.planet)
planet() {
return {
list: implement(contract.planet.list).handler(({ input }) => {
// Implement logic here
}),
find: implement(contract.planet.find).handler(({ input }) => {
// Implement logic here
}),
create: implement(contract.planet.create).handler(({ input }) => {
// Implement logic here
}),
}
}
// other handlers...
}WARNING
If you using @Implement decorator for router contract, underhook it creates corresponding NestJS method for each procedure contract. Therefore, all other decorator should be applied before @Implement decorator, otherwise it will not be applied to corresponding NestJS methods.
@Controller()
export class PlanetController {
@Implement(contract.planet) // ⬇️ other decorators should be below this line
@UseGuards(AuthGuard)
planet(@Req() req: Request) {
return {
// your implementation
}
}
}Error Handling
By default, errors thrown in implemented procedures are caught and handled by oRPC, which then rethrows a generic HttpException to NestJS. If you want NestJS to catch the original error instead of HttpException, use the Rethrow Plugin to bypass oRPC error handling and let NestJS handle the error directly.
Body Parser
oRPC uses bodies parsed by NestJS when available, and falls back to its own parser otherwise. In some cases, you may want to disable the NestJS body parser so oRPC can handle parsing directly:
- NestJS
urlencodedparsing does not support Bracket Notation. - File uploads with common content types like
application/jsonmay not be parsed asFileinstances.
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)
}Configuration
Configure @orpc/nest by importing ORPCModule into your NestJS module. It supports the same options as the OpenAPI Handler, except for options that are unrelated to NestJS and options that are specific to NestJS.
import { onError } from '@orpc/server'
import { ORPCModule } from '@orpc/nest'
@Module({
imports: [
ORPCModule.forRoot({
interceptors: [
onError((error) => {
console.error(error)
}),
],
}),
],
})
export class AppModule {}Initial Context
To define initial context for use in oRPC scopes, extend the DefaultInitialContext interface and provide context through ORPCModule.forRootAsync (or ORPCModule.forRoot for static configuration).
declare module '@orpc/server' {
/**
* Extend the context interface to enable typesafe access across oRPC scopes
*/
interface DefaultInitialContext {
request: Request
}
}
@Module({
imports: [
ORPCModule.forRootAsync({
inject: [REQUEST],
useFactory: (request: Request) => ({
context: { request },
}),
}),
],
})
export class AppModule {}Plugins
Most handler plugins also work in NestJS, for example Request Headers, Response Headers, Rethrow, and Smart Coercion.
@Module({
imports: [
ORPCModule.forRoot({
plugins: [
new RethrowHandlerPlugin({
// Bypass oRPC error handling and let NestJS handle the error instead
filter: error => !(error instanceof ORPCError)
}),
],
}),
],
})
export class AppModule {}WARNING
Procedures run only when a matching NestJS controller method is called. If no route matches (404), neither the procedure nor its plugins run. As a result, plugins like OpenAPI Reference may not work as expected, since NestJS can respond with 404 before the procedure runs.
Event Stream Options
Configure how event iterators are streamed to the client using the toNestResponse.eventStream options.
@Module({
imports: [
ORPCModule.forRoot({
toNestResponse: {
eventStream: {
initialComment: {
/**
* If true, an initial comment is sent immediately upon stream start to flush headers.
* This allows the receiving side to establish the connection without waiting for the first event.
*
* @default true
*/
enabled: true,
/**
* The content of the initial comment sent upon stream start. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
keepAlive: {
/**
* If true, a ping comment is sent periodically to keep the connection alive.
*
* @default true
*/
enabled: true,
/**
* Interval (in milliseconds) between ping comments sent after the last event.
*
* @default 5000
*/
interval: 5000,
/**
* The content of the ping comment. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
/**
* If true, a `close` event is sent even when the iterator completes with `undefined`.
* When the iterator returns a value, a `close` event is always emitted regardless of this setting.
*
* @default true
*/
emptyCloseEventEnabled: true,
},
},
}),
],
})
export class AppModule {}toStandardLazyRequest option
By default, @orpc/nest supports the Express and Fastify adapters. If you use another adapter, you may need to customize how a NestJS request is converted into a standard request. For details, see Standard Server.
@Module({
imports: [
ORPCModule.forRoot({
toStandardLazyRequest: (req, res) => {
// your custom implementation
},
}),
],
})
export class AppModule {}Typesafe Client
After implementing your contract in NestJS, you can use the same contract to create a typesafe client. See OpenAPI Link for more details.

