Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,35 @@ async function bootstrap() {
oRPC will use NestJS parsed body when it's available, and only use the oRPC parser if the body is not parsed by NestJS.
:::

## Configuration

Configure the `@orpc/nest` module by importing `ORPCModule` in your NestJS application:

```ts
import { onError, ORPCModule } from '@orpc/nest'

@Module({
imports: [
ORPCModule.forRoot({
interceptors: [
onError((error) => {
console.error(error)
}),
],
eventIteratorKeepAliveInterval: 5000, // 5 seconds
}),
],
})
export class AppModule {}
```

::: info

- **`interceptors`** - [Server-side client interceptors](/docs/client/server-side#lifecycle) for intercepting input, output, and errors.
- **`eventIteratorKeepAliveInterval`** - Keep-alive interval for event streams (see [Event Iterator Keep Alive](/docs/rpc-handler#event-iterator-keep-alive))

:::

## 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](/docs/openapi/client/openapi-link), to interact with your API in a type-safe way.
Expand Down
13 changes: 13 additions & 0 deletions packages/nest/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
rollup: {
esbuild: {
tsconfigRaw: {
compilerOptions: {
experimentalDecorators: true,
},
},
},
},
})
38 changes: 37 additions & 1 deletion packages/nest/src/implement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { FastifyAdapter } from '@nestjs/platform-fastify'
import { Test } from '@nestjs/testing'
import { oc, ORPCError } from '@orpc/contract'
import { implement, lazy } from '@orpc/server'
import * as StandardServerNode from '@orpc/standard-server-node'
import supertest from 'supertest'
import { it, vi } from 'vitest'
import { expect, it, vi } from 'vitest'
import { z } from 'zod'
import { Implement } from './implement'
import { ORPCModule } from './module'

const sendStandardResponseSpy = vi.spyOn(StandardServerNode, 'sendStandardResponse')

beforeEach(() => {
vi.clearAllMocks()
Expand Down Expand Up @@ -376,4 +380,36 @@ describe('@Implement', async () => {
false,
])
})

it('works with ORPCModule.forRoot', async () => {
const interceptor = vi.fn(({ next }) => next())
const moduleRef = await Test.createTestingModule({
imports: [
ORPCModule.forRoot({
interceptors: [interceptor],
eventIteratorKeepAliveComment: '__TEST__',
}),
],
controllers: [ImplProcedureController],
}).compile()

const app = moduleRef.createNestApplication()
await app.init()

const httpServer = app.getHttpServer()

const res = await supertest(httpServer)
.post('/ping?param=value&param2[]=value2&param2[]=value3')
.set('x-custom', 'value')
.send({ hello: 'world' })

expect(res.statusCode).toEqual(200)
expect(res.body).toEqual('pong')

expect(interceptor).toHaveBeenCalledTimes(1)
expect(sendStandardResponseSpy).toHaveBeenCalledTimes(1)
expect(sendStandardResponseSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({
eventIteratorKeepAliveComment: '__TEST__',
}))
})
})
14 changes: 11 additions & 3 deletions packages/nest/src/implement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import type { NodeHttpRequest, NodeHttpResponse } from '@orpc/standard-server-no
import type { Request, Response } from 'express'
import type { FastifyReply, FastifyRequest } from 'fastify'
import type { Observable } from 'rxjs'
import { applyDecorators, Delete, Get, Head, Patch, Post, Put, UseInterceptors } from '@nestjs/common'
import type { ORPCModuleConfig } from './module'
import { applyDecorators, Delete, Get, Head, Inject, Injectable, Optional, Patch, Post, Put, UseInterceptors } from '@nestjs/common'
import { toORPCError } from '@orpc/client'
import { fallbackContractConfig, isContractProcedure } from '@orpc/contract'
import { StandardBracketNotationSerializer, StandardOpenAPIJsonSerializer, StandardOpenAPISerializer } from '@orpc/openapi-client/standard'
Expand All @@ -18,6 +19,7 @@ import { get } from '@orpc/shared'
import { flattenHeader } from '@orpc/standard-server'
import { sendStandardResponse, toStandardLazyRequest } from '@orpc/standard-server-node'
import { mergeMap } from 'rxjs'
import { ORPC_MODULE_CONFIG_SYMBOL } from './module'
import { toNestPattern } from './utils'

const MethodDecoratorMap = {
Expand Down Expand Up @@ -97,7 +99,13 @@ const codec = new StandardOpenAPICodec(

type NestParams = Record<string, string | string[]>

@Injectable()
export class ImplementInterceptor implements NestInterceptor {
constructor(
@Inject(ORPC_MODULE_CONFIG_SYMBOL) @Optional() private readonly config: ORPCModuleConfig | undefined,
) {
}

intercept(ctx: ExecutionContext, next: CallHandler<any>): Observable<any> {
return next.handle().pipe(
mergeMap(async (impl: unknown) => {
Expand All @@ -124,7 +132,7 @@ export class ImplementInterceptor implements NestInterceptor {
let isDecoding = false

try {
const client = createProcedureClient(procedure)
const client = createProcedureClient(procedure, this.config)

isDecoding = true
const input = await codec.decode(standardRequest, flattenParams(req.params as NestParams), procedure)
Expand All @@ -149,7 +157,7 @@ export class ImplementInterceptor implements NestInterceptor {
}
})()

await sendStandardResponse(nodeRes, standardResponse)
await sendStandardResponse(nodeRes, standardResponse, this.config)
}),
)
}
Expand Down
3 changes: 2 additions & 1 deletion packages/nest/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export * from './implement'
export { Implement as Impl } from './implement'
export * from './module'

Check warning on line 3 in packages/nest/src/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/nest/src/index.ts#L3

Added line #L3 was not covered by tests
export * from './utils'

export { implement, ORPCError } from '@orpc/server'
export { implement, onError, onFinish, onStart, onSuccess, ORPCError } from '@orpc/server'

Check warning on line 6 in packages/nest/src/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/nest/src/index.ts#L6

Added line #L6 was not covered by tests
export type {
ImplementedProcedure,
Implementer,
Expand Down
31 changes: 31 additions & 0 deletions packages/nest/src/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { DynamicModule } from '@nestjs/common'
import type { AnySchema } from '@orpc/contract'
import type { CreateProcedureClientOptions } from '@orpc/server'
import type { SendStandardResponseOptions } from '@orpc/standard-server-node'
import { Module } from '@nestjs/common'
import { ImplementInterceptor } from './implement'

export const ORPC_MODULE_CONFIG_SYMBOL = Symbol('ORPC_MODULE_CONFIG')

export interface ORPCModuleConfig extends
CreateProcedureClientOptions<object, AnySchema, object, object, object>,
SendStandardResponseOptions {
}

@Module({})
export class ORPCModule {
static forRoot(config: ORPCModuleConfig): DynamicModule {
return {
module: ORPCModule,
providers: [
{
provide: ORPC_MODULE_CONFIG_SYMBOL,
useValue: config,
},
ImplementInterceptor,
],
exports: [ORPC_MODULE_CONFIG_SYMBOL, ImplementInterceptor],
global: true,
}
}
}
1 change: 1 addition & 0 deletions packages/nest/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.lib.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
},
"references": [
Expand Down
18 changes: 18 additions & 0 deletions playgrounds/nest/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
entries: [
{ input: 'dist/main.js', outDir: 'dist/unbuild', name: 'main' },
],
failOnWarn: false,
clean: false,
rollup: {
esbuild: {
tsconfigRaw: {
compilerOptions: {
experimentalDecorators: true,
},
},
},
},
})
4 changes: 2 additions & 2 deletions playgrounds/nest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.6.0",
"private": true,
"scripts": {
"preview": "nest build && tsx dist/main.js",
"preview": "nest build && unbuild --stub && node dist/main.mjs",
"start:dev": "nest start --watch",
"type:check": "tsc --noEmit"
},
Expand Down Expand Up @@ -32,8 +32,8 @@
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"zod": "^3.25.67"
}
}
12 changes: 11 additions & 1 deletion playgrounds/nest/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,19 @@ import { OtherController } from './other/other.controller'
import { PlanetService } from './planet/planet.service'
import { ReferenceController } from './reference/reference.controller'
import { ReferenceService } from './reference/reference.service'
import { onError, ORPCModule } from '@orpc/nest'

@Module({
imports: [],
imports: [
ORPCModule.forRoot({
interceptors: [
onError((error) => {
console.error(error)
}),
],
eventIteratorKeepAliveInterval: 5000, // 5 seconds
}),
],
controllers: [AuthController, PlanetController, ReferenceController, OtherController],
providers: [PlanetService, ReferenceService],
})
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.