Skip to content

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

sh
npm install @orpc/nest@latest
sh
yarn add @orpc/nest@latest
sh
pnpm add @orpc/nest@latest
sh
bun add @orpc/nest@latest
sh
deno install npm:@orpc/nest@latest

Requirements

oRPC is an ESM-only library. Therefore, your NestJS application must be configured to support ESM modules.

  1. 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,
      }
    }
  2. 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.

    WARNING

    By default, NestJS bundler (Webpack or SWC) might not compile node_modules. You may need to adjust your bundler configs to include @orpc/nest for compilation.

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
ts
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

ts
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:

ts
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.

typescript
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.

Released under the MIT License.