Skip to content

Commit f56f7a0

Browse files
committed
fixup! feat(server): Change the nest integration to support Nest features that were competing with Orpc
1 parent 047a5b7 commit f56f7a0

File tree

7 files changed

+1038
-202
lines changed

7 files changed

+1038
-202
lines changed

packages/nest/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,23 @@
6161
"@orpc/standard-server-node": "workspace:*"
6262
},
6363
"devDependencies": {
64+
"@fastify/compress": "^8.1.0",
6465
"@fastify/cookie": "^11.0.2",
6566
"@nestjs/common": "^11.1.8",
6667
"@nestjs/core": "^11.1.8",
6768
"@nestjs/platform-express": "^11.1.8",
6869
"@nestjs/platform-fastify": "^11.1.8",
6970
"@nestjs/testing": "^11.1.8",
7071
"@ts-rest/core": "^3.52.1",
72+
"@types/compression": "^1.8.1",
7173
"@types/express": "^5.0.5",
74+
"@types/node": "^22.15.30",
75+
"@types/supertest": "^6.0.3",
76+
"compression": "^1.8.1",
7277
"express": "^5.0.0",
7378
"fastify": "^5.6.1",
7479
"rxjs": "^7.8.1",
7580
"supertest": "^7.1.4",
7681
"zod": "^4.1.12"
7782
}
78-
}
83+
}

packages/nest/src/implement.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,8 @@ export class ImplementInterceptor implements NestInterceptor {
123123
? StandardServerFastify.toStandardLazyRequest(req, res as FastifyReply)
124124
: StandardServerNode.toStandardLazyRequest(req, res as Response)
125125

126-
// Pass the original NestJS request as context
127-
const contextWithRequest = {
128-
...(this.config?.context || {}),
129-
request: req,
130-
response: res,
131-
}
126+
const client = createProcedureClient(procedure, this.config)
132127

133-
const client = createProcedureClient(procedure, {
134-
...this.config,
135-
context: contextWithRequest,
136-
})
137128
const standardResponse: StandardResponse = await (async (): Promise<StandardResponse> => {
138129
// Decode input - catch only non-ORPC decoding errors and convert to ORPCError
139130
let input: Awaited<ReturnType<typeof codec.decode>>

packages/nest/src/nest-features.test.ts

Lines changed: 81 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import type { ArgumentsHost, CallHandler, CanActivate, ExceptionFilter, ExecutionContext, INestApplication, MiddlewareConsumer, NestInterceptor, NestMiddleware, PipeTransform } from '@nestjs/common'
2+
import type { NestFastifyApplication } from '@nestjs/platform-fastify'
23
import type { Observable } from 'rxjs'
3-
import { Catch, Controller, ForbiddenException, HttpException, Injectable, Module, SetMetadata, UseGuards, UseInterceptors } from '@nestjs/common'
4+
import fastifyCompress from '@fastify/compress'
5+
import { Catch, Controller, ForbiddenException, HttpException, Injectable, Module, Req, SetMetadata, UseGuards, UseInterceptors } from '@nestjs/common'
46
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'
57
import { ExpressAdapter } from '@nestjs/platform-express'
68
import { FastifyAdapter } from '@nestjs/platform-fastify'
79
import { Test } from '@nestjs/testing'
810
import { oc } from '@orpc/contract'
911
import { implement } from '@orpc/server'
1012
import * as StandardServerNode from '@orpc/standard-server-node'
13+
// eslint-disable-next-line no-restricted-imports -- needed for testing compression middleware integration
14+
import compression from 'compression'
1115
import { map } from 'rxjs'
1216
import request from 'supertest'
1317
import { afterEach, describe, expect, it, vi } from 'vitest'
1418
import { z } from 'zod'
1519

16-
import { Implement, ORPCExceptionFilter, ORPCModule } from '.'
20+
import { Implement, ORPCModule } from '.'
1721

1822
// 1. oRPC Contract
1923
const testContract = {
@@ -71,7 +75,16 @@ const testGuardContract = {
7175
.output(z.object({ message: z.string(), user: z.string() })),
7276
}
7377

74-
// 2. A real controller for the 'raw' output test
78+
// Contract for testing compression middleware
79+
const testCompressionContract = {
80+
data: oc.route({
81+
path: '/large-data',
82+
method: 'GET',
83+
})
84+
.output(z.object({ data: z.string(), size: z.number() })),
85+
}
86+
87+
// 2. A controller for the 'raw' output test
7588
@Controller()
7689
class TestRawController {
7790
@Implement(testContract.hello)
@@ -98,7 +111,7 @@ class TestDetailedController {
98111
}
99112
}
100113

101-
// 4. Interceptor that modifies the response body (must be declared before controllers that use it)
114+
// 4. Interceptor that modifies the response body
102115
@Injectable()
103116
class ResponseTransformInterceptor implements NestInterceptor {
104117
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
@@ -113,8 +126,7 @@ class ResponseTransformInterceptor implements NestInterceptor {
113126
)
114127
}
115128
}
116-
117-
// 5. Controller with interceptor to test response transformation
129+
// 4. Controller with interceptor to test response transformation
118130
@Controller()
119131
class TestInterceptorController {
120132
@Implement(testContract.hello)
@@ -193,20 +205,18 @@ class AuthenticatedGuard implements CanActivate {
193205
}
194206

195207
// 11. Controller for testing guards (method-level)
196-
// IMPORTANT: The @Implement interceptor passes the NestJS request/response
197-
// as part of the oRPC context, allowing handlers to access guard modifications
198-
// like properties set on the original request (standard practice).
199-
// This solves the limitation where oRPC creates its own standardized request object.
208+
// Also try accessing request modifications made by guards
200209
@Controller()
201210
class TestGuardController {
202211
@UseGuards(ApiKeyGuard, AuthenticatedGuard)
203212
@RequireRole('admin')
204213
@Implement(testGuardContract.protected)
205-
protected() {
206-
return implement(testGuardContract.protected).handler(async ({ input, context }) => {
207-
const req = (context as any).request
214+
protected(
215+
@Req()
216+
req: any,
217+
) {
218+
return implement(testGuardContract.protected).handler(async () => {
208219
const user = req?.user
209-
210220
return {
211221
message: 'Access granted',
212222
user: user?.name || 'Unknown',
@@ -235,6 +245,22 @@ class UpperCasePipe implements PipeTransform {
235245
}
236246
}
237247

248+
// 13. Controller for testing compression middleware
249+
@Controller()
250+
class TestCompressionController {
251+
@Implement(testCompressionContract.data)
252+
data() {
253+
return implement(testCompressionContract.data).handler(async () => {
254+
// Return a response that can be compressed (1kb minimum)
255+
const largeData = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(50)
256+
return {
257+
data: largeData,
258+
size: largeData.length,
259+
}
260+
})
261+
}
262+
}
263+
238264
// 9. Global interceptor that modifies the response
239265
@Injectable()
240266
class GlobalLoggingInterceptor implements NestInterceptor {
@@ -249,7 +275,7 @@ class GlobalLoggingInterceptor implements NestInterceptor {
249275
}
250276
}
251277

252-
// 9. Global filter that catches HTTP exceptions
278+
// 10. Global filter that catches HTTP exceptions
253279
@Catch(HttpException)
254280
class GlobalHttpExceptionFilter implements ExceptionFilter {
255281
catch(exception: HttpException, host: ArgumentsHost) {
@@ -269,23 +295,18 @@ class GlobalHttpExceptionFilter implements ExceptionFilter {
269295
}
270296
}
271297

272-
// 10. Custom Middleware
298+
// 11. Custom Middleware
273299
class CustomHeaderMiddleware implements NestMiddleware {
274300
use(req: any, res: any, next: (error?: any) => void) {
275301
res.setHeader('X-Custom-Middleware', 'hello')
276302
next()
277303
}
278304
}
279305

280-
// 10. Test Modules for each controller
306+
// Test Modules for each controller
281307
@Module({
282308
controllers: [TestRawController],
283-
providers: [
284-
{
285-
provide: APP_FILTER,
286-
useClass: ORPCExceptionFilter,
287-
},
288-
],
309+
providers: [],
289310
})
290311
class TestRawModule {
291312
configure(consumer: MiddlewareConsumer) {
@@ -295,12 +316,7 @@ class TestRawModule {
295316

296317
@Module({
297318
controllers: [TestDetailedController],
298-
providers: [
299-
{
300-
provide: APP_FILTER,
301-
useClass: ORPCExceptionFilter,
302-
},
303-
],
319+
providers: [],
304320
})
305321
class TestDetailedModule {
306322
configure(consumer: MiddlewareConsumer) {
@@ -312,10 +328,6 @@ class TestDetailedModule {
312328
controllers: [TestInterceptorController],
313329
providers: [
314330
ResponseTransformInterceptor,
315-
{
316-
provide: APP_FILTER,
317-
useClass: ORPCExceptionFilter,
318-
},
319331
],
320332
})
321333
class TestInterceptorModule {
@@ -331,10 +343,6 @@ class TestInterceptorModule {
331343
provide: APP_PIPE,
332344
useClass: UpperCasePipe,
333345
},
334-
{
335-
provide: APP_FILTER,
336-
useClass: ORPCExceptionFilter,
337-
},
338346
],
339347
})
340348
class TestPipeModule {
@@ -350,23 +358,13 @@ class TestPipeModule {
350358
provide: APP_FILTER,
351359
useClass: GlobalHttpExceptionFilter,
352360
},
353-
// this will not run because GlobalHttpExceptionFilter sends the response first
354-
{
355-
provide: APP_FILTER,
356-
useClass: ORPCExceptionFilter,
357-
},
358361
],
359362
})
360363
class TestErrorModule {}
361364

362365
@Module({
363366
controllers: [TestGuardController],
364-
providers: [
365-
{
366-
provide: APP_FILTER,
367-
useClass: ORPCExceptionFilter,
368-
},
369-
],
367+
providers: [],
370368
})
371369
class TestGuardModule {
372370
configure(consumer: MiddlewareConsumer) {
@@ -381,10 +379,6 @@ class TestGuardModule {
381379
provide: APP_INTERCEPTOR,
382380
useClass: GlobalLoggingInterceptor,
383381
},
384-
{
385-
provide: APP_FILTER,
386-
useClass: ORPCExceptionFilter,
387-
},
388382
],
389383
})
390384
class TestGlobalInterceptorModule {
@@ -393,6 +387,16 @@ class TestGlobalInterceptorModule {
393387
}
394388
}
395389

390+
@Module({
391+
controllers: [TestCompressionController],
392+
})
393+
class TestCompressionModule {
394+
configure(consumer: MiddlewareConsumer) {
395+
// Use the actual compression middleware for Express
396+
consumer.apply(compression()).forRoutes('*')
397+
}
398+
}
399+
396400
const sendStandardResponseSpy = vi.spyOn(StandardServerNode, 'sendStandardResponse')
397401

398402
describe('oRPC Nest Middleware Integration', () => {
@@ -410,6 +414,12 @@ describe('oRPC Nest Middleware Integration', () => {
410414

411415
app = moduleFixture.createNestApplication(adapter())
412416
app.enableCors()
417+
418+
// Register compression for Fastify
419+
if (adapterName === 'Fastify') {
420+
await (app as NestFastifyApplication).register(fastifyCompress)
421+
}
422+
413423
await app.init()
414424
if (adapterName === 'Fastify') {
415425
await (app as any).getHttpAdapter().getInstance().ready()
@@ -576,6 +586,25 @@ describe('oRPC Nest Middleware Integration', () => {
576586
expect(response.body.globalInterceptor).toBe(true)
577587
})
578588
})
589+
590+
it('should work with compression middleware that accesses oRPC response body', async () => {
591+
await createApp(TestCompressionModule, {})
592+
593+
// Make a request with Accept-Encoding header to enable compression
594+
const response = await request(app.getHttpServer())
595+
.get('/large-data')
596+
.set('Accept-Encoding', 'gzip, deflate')
597+
.expect(200)
598+
599+
// Verify compression was applied (check for content-encoding header)
600+
// Note: compression middleware only compresses responses above a certain threshold (default 1kb)
601+
expect(['gzip', 'deflate']).toContain(response.headers['content-encoding'])
602+
603+
// Verify that the oRPC handler response is correctly returned (supertest auto-decompresses)
604+
expect(response.body).toHaveProperty('data')
605+
expect(response.body).toHaveProperty('size')
606+
expect(response.body.size).toBeGreaterThan(0)
607+
})
579608
})
580609
}
581610

0 commit comments

Comments
 (0)