Skip to content

Commit 3d8a81d

Browse files
authored
feat(nest): module configuration (#710)
Closes: https://github.com/unnoq/orpc/issues/709 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a configurable ORPCModule for NestJS, enabling centralized setup of error interceptors and event stream keep-alive intervals. * Expanded main exports to include new configuration and lifecycle hooks for greater flexibility. * Added comprehensive documentation and usage examples for configuring oRPC contracts in NestJS. * **Bug Fixes** * None. * **Tests** * Added new test case to verify integration and configuration of ORPCModule within a NestJS application. * **Chores** * Updated build and TypeScript configurations to support experimental decorators and improved build processes in both the core package and playground. * Updated playground scripts and dependencies for streamlined preview and build. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent d636350 commit 3d8a81d

11 files changed

Lines changed: 158 additions & 11 deletions

File tree

apps/content/docs/openapi/integrations/implement-contract-in-nest.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,35 @@ async function bootstrap() {
220220
oRPC will use NestJS parsed body when it's available, and only use the oRPC parser if the body is not parsed by NestJS.
221221
:::
222222

223+
## Configuration
224+
225+
Configure the `@orpc/nest` module by importing `ORPCModule` in your NestJS application:
226+
227+
```ts
228+
import { onError, ORPCModule } from '@orpc/nest'
229+
230+
@Module({
231+
imports: [
232+
ORPCModule.forRoot({
233+
interceptors: [
234+
onError((error) => {
235+
console.error(error)
236+
}),
237+
],
238+
eventIteratorKeepAliveInterval: 5000, // 5 seconds
239+
}),
240+
],
241+
})
242+
export class AppModule {}
243+
```
244+
245+
::: info
246+
247+
- **`interceptors`** - [Server-side client interceptors](/docs/client/server-side#lifecycle) for intercepting input, output, and errors.
248+
- **`eventIteratorKeepAliveInterval`** - Keep-alive interval for event streams (see [Event Iterator Keep Alive](/docs/rpc-handler#event-iterator-keep-alive))
249+
250+
:::
251+
223252
## Create a Type-Safe Client
224253

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

packages/nest/build.config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defineBuildConfig } from 'unbuild'
2+
3+
export default defineBuildConfig({
4+
rollup: {
5+
esbuild: {
6+
tsconfigRaw: {
7+
compilerOptions: {
8+
experimentalDecorators: true,
9+
},
10+
},
11+
},
12+
},
13+
})

packages/nest/src/implement.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import { FastifyAdapter } from '@nestjs/platform-fastify'
44
import { Test } from '@nestjs/testing'
55
import { oc, ORPCError } from '@orpc/contract'
66
import { implement, lazy } from '@orpc/server'
7+
import * as StandardServerNode from '@orpc/standard-server-node'
78
import supertest from 'supertest'
8-
import { it, vi } from 'vitest'
9+
import { expect, it, vi } from 'vitest'
910
import { z } from 'zod'
1011
import { Implement } from './implement'
12+
import { ORPCModule } from './module'
13+
14+
const sendStandardResponseSpy = vi.spyOn(StandardServerNode, 'sendStandardResponse')
1115

1216
beforeEach(() => {
1317
vi.clearAllMocks()
@@ -376,4 +380,36 @@ describe('@Implement', async () => {
376380
false,
377381
])
378382
})
383+
384+
it('works with ORPCModule.forRoot', async () => {
385+
const interceptor = vi.fn(({ next }) => next())
386+
const moduleRef = await Test.createTestingModule({
387+
imports: [
388+
ORPCModule.forRoot({
389+
interceptors: [interceptor],
390+
eventIteratorKeepAliveComment: '__TEST__',
391+
}),
392+
],
393+
controllers: [ImplProcedureController],
394+
}).compile()
395+
396+
const app = moduleRef.createNestApplication()
397+
await app.init()
398+
399+
const httpServer = app.getHttpServer()
400+
401+
const res = await supertest(httpServer)
402+
.post('/ping?param=value&param2[]=value2&param2[]=value3')
403+
.set('x-custom', 'value')
404+
.send({ hello: 'world' })
405+
406+
expect(res.statusCode).toEqual(200)
407+
expect(res.body).toEqual('pong')
408+
409+
expect(interceptor).toHaveBeenCalledTimes(1)
410+
expect(sendStandardResponseSpy).toHaveBeenCalledTimes(1)
411+
expect(sendStandardResponseSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({
412+
eventIteratorKeepAliveComment: '__TEST__',
413+
}))
414+
})
379415
})

packages/nest/src/implement.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import type { NodeHttpRequest, NodeHttpResponse } from '@orpc/standard-server-no
88
import type { Request, Response } from 'express'
99
import type { FastifyReply, FastifyRequest } from 'fastify'
1010
import type { Observable } from 'rxjs'
11-
import { applyDecorators, Delete, Get, Head, Patch, Post, Put, UseInterceptors } from '@nestjs/common'
11+
import type { ORPCModuleConfig } from './module'
12+
import { applyDecorators, Delete, Get, Head, Inject, Injectable, Optional, Patch, Post, Put, UseInterceptors } from '@nestjs/common'
1213
import { toORPCError } from '@orpc/client'
1314
import { fallbackContractConfig, isContractProcedure } from '@orpc/contract'
1415
import { StandardBracketNotationSerializer, StandardOpenAPIJsonSerializer, StandardOpenAPISerializer } from '@orpc/openapi-client/standard'
@@ -18,6 +19,7 @@ import { get } from '@orpc/shared'
1819
import { flattenHeader } from '@orpc/standard-server'
1920
import { sendStandardResponse, toStandardLazyRequest } from '@orpc/standard-server-node'
2021
import { mergeMap } from 'rxjs'
22+
import { ORPC_MODULE_CONFIG_SYMBOL } from './module'
2123
import { toNestPattern } from './utils'
2224

2325
const MethodDecoratorMap = {
@@ -97,7 +99,13 @@ const codec = new StandardOpenAPICodec(
9799

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

102+
@Injectable()
100103
export class ImplementInterceptor implements NestInterceptor {
104+
constructor(
105+
@Inject(ORPC_MODULE_CONFIG_SYMBOL) @Optional() private readonly config: ORPCModuleConfig | undefined,
106+
) {
107+
}
108+
101109
intercept(ctx: ExecutionContext, next: CallHandler<any>): Observable<any> {
102110
return next.handle().pipe(
103111
mergeMap(async (impl: unknown) => {
@@ -124,7 +132,7 @@ export class ImplementInterceptor implements NestInterceptor {
124132
let isDecoding = false
125133

126134
try {
127-
const client = createProcedureClient(procedure)
135+
const client = createProcedureClient(procedure, this.config)
128136

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

152-
await sendStandardResponse(nodeRes, standardResponse)
160+
await sendStandardResponse(nodeRes, standardResponse, this.config)
153161
}),
154162
)
155163
}

packages/nest/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
export * from './implement'
22
export { Implement as Impl } from './implement'
3+
export * from './module'
34
export * from './utils'
45

5-
export { implement, ORPCError } from '@orpc/server'
6+
export { implement, onError, onFinish, onStart, onSuccess, ORPCError } from '@orpc/server'
67
export type {
78
ImplementedProcedure,
89
Implementer,

packages/nest/src/module.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { DynamicModule } from '@nestjs/common'
2+
import type { AnySchema } from '@orpc/contract'
3+
import type { CreateProcedureClientOptions } from '@orpc/server'
4+
import type { SendStandardResponseOptions } from '@orpc/standard-server-node'
5+
import { Module } from '@nestjs/common'
6+
import { ImplementInterceptor } from './implement'
7+
8+
export const ORPC_MODULE_CONFIG_SYMBOL = Symbol('ORPC_MODULE_CONFIG')
9+
10+
export interface ORPCModuleConfig extends
11+
CreateProcedureClientOptions<object, AnySchema, object, object, object>,
12+
SendStandardResponseOptions {
13+
}
14+
15+
@Module({})
16+
export class ORPCModule {
17+
static forRoot(config: ORPCModuleConfig): DynamicModule {
18+
return {
19+
module: ORPCModule,
20+
providers: [
21+
{
22+
provide: ORPC_MODULE_CONFIG_SYMBOL,
23+
useValue: config,
24+
},
25+
ImplementInterceptor,
26+
],
27+
exports: [ORPC_MODULE_CONFIG_SYMBOL, ImplementInterceptor],
28+
global: true,
29+
}
30+
}
31+
}

packages/nest/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"extends": "../../tsconfig.lib.json",
33
"compilerOptions": {
4+
"emitDecoratorMetadata": true,
45
"experimentalDecorators": true
56
},
67
"references": [

playgrounds/nest/build.config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { defineBuildConfig } from 'unbuild'
2+
3+
export default defineBuildConfig({
4+
entries: [
5+
{ input: 'dist/main.js', outDir: 'dist/unbuild', name: 'main' },
6+
],
7+
failOnWarn: false,
8+
clean: false,
9+
rollup: {
10+
esbuild: {
11+
tsconfigRaw: {
12+
compilerOptions: {
13+
experimentalDecorators: true,
14+
},
15+
},
16+
},
17+
},
18+
})

playgrounds/nest/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "1.6.0",
44
"private": true,
55
"scripts": {
6-
"preview": "nest build && tsx dist/main.js",
6+
"preview": "nest build && unbuild --stub && node dist/main.mjs",
77
"start:dev": "nest start --watch",
88
"type:check": "tsc --noEmit"
99
},
@@ -32,8 +32,8 @@
3232
"ts-loader": "^9.5.2",
3333
"ts-node": "^10.9.2",
3434
"tsconfig-paths": "^4.2.0",
35-
"tsx": "^4.20.3",
3635
"typescript": "^5.8.3",
36+
"unbuild": "^3.5.0",
3737
"zod": "^3.25.67"
3838
}
3939
}

playgrounds/nest/src/app.module.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,19 @@ import { OtherController } from './other/other.controller'
55
import { PlanetService } from './planet/planet.service'
66
import { ReferenceController } from './reference/reference.controller'
77
import { ReferenceService } from './reference/reference.service'
8+
import { onError, ORPCModule } from '@orpc/nest'
89

910
@Module({
10-
imports: [],
11+
imports: [
12+
ORPCModule.forRoot({
13+
interceptors: [
14+
onError((error) => {
15+
console.error(error)
16+
}),
17+
],
18+
eventIteratorKeepAliveInterval: 5000, // 5 seconds
19+
}),
20+
],
1121
controllers: [AuthController, PlanetController, ReferenceController, OtherController],
1222
providers: [PlanetService, ReferenceService],
1323
})

0 commit comments

Comments
 (0)