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
2 changes: 1 addition & 1 deletion apps/content/docs/integrations/nest.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export class AppModule {}

### `toNestStandardLazyRequest` 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 (including additional params). For details, see [Standard Server](https://github.com/middleapi/standard-server#request-and-response-types).
By default, `@orpc/nest` supports the Express, Fastify, and Node (`@nestjs/platform-node`, NestJS v12.3+) adapters. If you use another adapter, you may need to customize how a NestJS request is converted into a standard request (including additional params). For details, see [Standard Server](https://github.com/middleapi/standard-server#request-and-response-types).

```ts
import { NestStandardLazyRequest } from '@orpc/nest'
Expand Down
15 changes: 10 additions & 5 deletions packages/nest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,15 @@
"peerDependencies": {
"@nestjs/common": ">=11.1.27",
"@nestjs/core": ">=11.1.27",
"@nestjs/platform-node": ">=12.3.0",
"express": ">=5.2.1",
"fastify": ">=5.8.5",
"rxjs": ">=7.8.2"
},
"peerDependenciesMeta": {
"@nestjs/platform-node": {
"optional": true
},
"express": {
"optional": true
},
Expand All @@ -72,11 +76,12 @@
},
"devDependencies": {
"@fastify/cookie": "^11.1.3",
"@nestjs/common": "^12.1.3",
"@nestjs/core": "^12.1.3",
"@nestjs/platform-express": "^12.1.3",
"@nestjs/platform-fastify": "^12.1.3",
"@nestjs/testing": "^12.1.3",
"@nestjs/common": "^12.3.2",
"@nestjs/core": "^12.3.2",
"@nestjs/platform-express": "^12.3.2",
"@nestjs/platform-fastify": "^12.3.2",
"@nestjs/platform-node": "^12.3.2",
"@nestjs/testing": "^12.3.2",
"@types/express": "^5.0.6",
"express": "^5.3.0",
"fastify": "^5.12.7",
Expand Down
45 changes: 30 additions & 15 deletions packages/nest/src/implement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import FastifyCookie from '@fastify/cookie'
import { Controller, HttpException, Req, Res, SetMetadata, StreamableFile, UseGuards, UseInterceptors } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { FastifyAdapter } from '@nestjs/platform-fastify'
import { NodeAdapter } from '@nestjs/platform-node'
import { Test } from '@nestjs/testing'
import { meta, oc } from '@orpc/contract'
import { openapi } from '@orpc/openapi'
Expand Down Expand Up @@ -82,7 +83,10 @@ describe('requirements', () => {
}
})

it('should support the QUERY HTTP method when QueryMethod is available (NestJS 11.2+)', async () => {
it.each([
['express adapter', undefined],
['node adapter', new NodeAdapter()],
] as const)('should support the QUERY HTTP method when QueryMethod is available (NestJS 11.2+) with %s', async (_, adapter) => {
const contract = oc
.meta(openapi({
path: '/query-route',
Expand All @@ -102,7 +106,7 @@ describe('requirements', () => {
controllers: [QueryController],
}).compile()

const app = moduleRef.createNestApplication()
const app = moduleRef.createNestApplication(adapter as any)
await app.init()
await app.listen(0)

Expand Down Expand Up @@ -374,6 +378,7 @@ describe('routing', () => {
describe.each([
['express adapter', undefined],
['fastify adapter', new FastifyAdapter()],
['node adapter', new NodeAdapter()],
] as const)('with %s', async (_, adapter) => {
const moduleRef = await Test.createTestingModule({
controllers: [Controller],
Expand All @@ -382,7 +387,7 @@ describe('routing', () => {
const app = moduleRef.createNestApplication(adapter as any)
await app.init()

if (adapter) {
if (adapter instanceof FastifyAdapter) {
await app.getHttpAdapter().getInstance().ready()
}

Expand Down Expand Up @@ -455,6 +460,7 @@ describe('routing', () => {
describe.each([
['express adapter', undefined],
['fastify adapter', new FastifyAdapter()],
['node adapter', new NodeAdapter()],
] as const)('params edge cases with %s', async (_, adapter) => {
const contract = {
staticPath: oc.meta(openapi({
Expand Down Expand Up @@ -528,7 +534,7 @@ describe('routing', () => {
const app = moduleRef.createNestApplication(adapter as any)
await app.init()

if (adapter) {
if (adapter instanceof FastifyAdapter) {
await app.getHttpAdapter().getInstance().ready()
}

Expand Down Expand Up @@ -604,6 +610,7 @@ describe('response status, headers and body should follow standard-server', () =
describe.each([
['express adapter', undefined],
['fastify adapter', new FastifyAdapter()],
['node adapter', new NodeAdapter()],
] as const)('with %s', async (_, adapter) => {
const routingInterceptor = vi.fn(({ next }) => next())
const moduleRef = await Test.createTestingModule({
Expand All @@ -630,6 +637,9 @@ describe('response status, headers and body should follow standard-server', () =

const httpServer = app.getHttpServer()

// express and fastify append a charset to a content-type set by the response, the node adapter sends it as is
const charset = adapter instanceof NodeAdapter ? '' : '; charset=utf-8'

it('should response with output status', async () => {
handler
.mockResolvedValueOnce({ status: 202 })
Expand Down Expand Up @@ -685,7 +695,7 @@ describe('response status, headers and body should follow standard-server', () =
await expect(supertest(httpServer).post('/response')).resolves.toSatisfy((res) => {
expect(res.statusCode).toEqual(200)
expect(res.headers['standard-server']).toBeUndefined()
expect(res.headers['content-type']).toEqual('application/json; charset=utf-8')
expect(res.headers['content-type']).toEqual(`application/json${charset}`)
expect(res.text).toEqual('"string"')

return true
Expand All @@ -695,7 +705,7 @@ describe('response status, headers and body should follow standard-server', () =
await expect(supertest(httpServer).post('/response')).resolves.toSatisfy((res) => {
expect(res.statusCode).toEqual(200)
expect(res.headers['standard-server']).toBeUndefined()
expect(res.headers['content-type']).toEqual('application/json; charset=utf-8')
expect(res.headers['content-type']).toEqual(`application/json${charset}`)
expect(res.text).toEqual('null')

return true
Expand All @@ -705,7 +715,7 @@ describe('response status, headers and body should follow standard-server', () =
await expect(supertest(httpServer).post('/response')).resolves.toSatisfy((res) => {
expect(res.statusCode).toEqual(200)
expect(res.headers['standard-server']).toBeUndefined()
expect(res.headers['content-type']).toEqual('application/json; charset=utf-8')
expect(res.headers['content-type']).toEqual(`application/json${charset}`)
expect(res.text).toEqual('true')

// do not stringify if possible
Expand All @@ -718,7 +728,7 @@ describe('response status, headers and body should follow standard-server', () =
await expect(supertest(httpServer).post('/response')).resolves.toSatisfy((res) => {
expect(res.statusCode).toEqual(200)
expect(res.headers['standard-server']).toBeUndefined()
expect(res.headers['content-type']).toEqual('application/json; charset=utf-8')
expect(res.headers['content-type']).toEqual(`application/json${charset}`)
expect(res.text).toEqual('123')

// do not stringify if possible
Expand All @@ -731,7 +741,7 @@ describe('response status, headers and body should follow standard-server', () =
await expect(supertest(httpServer).post('/response')).resolves.toSatisfy((res) => {
expect(res.statusCode).toEqual(200)
expect(res.headers['standard-server']).toBeUndefined()
expect(res.headers['content-type']).toEqual('application/json; charset=utf-8')
expect(res.headers['content-type']).toEqual(`application/json${charset}`)
expect(res.text).toEqual('[1,2,3]')

// do not stringify if possible
Expand All @@ -744,7 +754,7 @@ describe('response status, headers and body should follow standard-server', () =
await expect(supertest(httpServer).post('/response')).resolves.toSatisfy((res) => {
expect(res.statusCode).toEqual(200)
expect(res.headers['standard-server']).toBeUndefined()
expect(res.headers['content-type']).toEqual('application/json; charset=utf-8')
expect(res.headers['content-type']).toEqual(`application/json${charset}`)
expect(res.text).toEqual('{"a":1,"b":2}')

// do not stringify if possible
Expand Down Expand Up @@ -972,7 +982,7 @@ describe('response status, headers and body should follow standard-server', () =
{
name: 'JSON with a JSON content-type',
response: () => ({ headers: { ...headers, 'content-type': 'application/json' }, body: { a: 1 } }),
expected: { 'standard-server': undefined, 'content-type': 'application/json; charset=utf-8', 'content-length': '7' },
expected: { 'standard-server': undefined, 'content-type': `application/json${charset}`, 'content-length': '7' },
text: '{"a":1}',
},
{
Expand Down Expand Up @@ -1081,6 +1091,7 @@ describe('error handling', () => {
describe.each([
['express adapter', undefined],
['fastify adapter', new FastifyAdapter()],
['node adapter', new NodeAdapter()],
] as const)('with %s', async (_, adapter) => {
const routingInterceptor = vi.fn(({ next }) => next())
const moduleRef = await Test.createTestingModule({
Expand All @@ -1105,12 +1116,15 @@ describe('error handling', () => {
}())
await app.init()

if (adapter) {
if (adapter instanceof FastifyAdapter) {
await app.getHttpAdapter().getInstance().ready()
}

const httpServer = app.getHttpServer()

// express and fastify append a charset to a content-type set by the response, the node adapter sends it as is
const charset = adapter instanceof NodeAdapter ? '' : '; charset=utf-8'

it('should throw HttpException for regular errors', async () => {
const error = new ORPCError('NOT_FOUND', { data: 'test data' })
handler.mockRejectedValueOnce(error)
Expand Down Expand Up @@ -1147,7 +1161,7 @@ describe('error handling', () => {
{
name: 'with',
headers: { 'content-type': 'application/problem+json' },
expected: { 'standard-server': 'json', 'content-type': 'application/problem+json; charset=utf-8' },
expected: { 'standard-server': 'json', 'content-type': `application/problem+json${charset}` },
},
])('should respect or override response headers of HttpException $name a content-type', async ({ headers, expected }) => {
routingInterceptor.mockResolvedValueOnce({
Expand Down Expand Up @@ -1204,7 +1218,7 @@ describe('error handling', () => {
await expect(supertest(httpServer).post('/error')).resolves.toSatisfy((res) => {
expect(res.statusCode).toEqual(502)
expect(res.text).toEqual('"text"')
expect(res.headers['content-type']).toEqual('application/json; charset=utf-8')
expect(res.headers['content-type']).toEqual(`application/json${charset}`)

return true
})
Expand Down Expand Up @@ -1789,6 +1803,7 @@ describe('compatibility', () => {
describe.each([
['express adapter', undefined],
['fastify adapter', new FastifyAdapter()],
['node adapter', new NodeAdapter()],
] as const)('with %s', async (_, adapter) => {
const moduleRef = await Test.createTestingModule({
controllers: [ImplController],
Expand All @@ -1802,7 +1817,7 @@ describe('compatibility', () => {
const app = moduleRef.createNestApplication(adapter as any)
await app.init()

if (adapter) {
if (adapter instanceof FastifyAdapter) {
await app.getHttpAdapter().getInstance().ready()
}

Expand Down
9 changes: 5 additions & 4 deletions packages/nest/src/implement.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'
import type { NodeRequest, NodeResponse } from '@nestjs/platform-node'
import type { AnyProcedureContract, RouterContract } from '@orpc/contract'
import type { AnyProcedure, Context, ContractedRouter, DefaultInitialContext } from '@orpc/server'
import type { ResolvedStandardHandlerHandleOptions, StandardHandlerCodec, StandardHandlerCodecResolvedProcedure } from '@orpc/server/standard'
Expand Down Expand Up @@ -174,7 +175,7 @@ export class ImplementInterceptor implements NestInterceptor {

this.codec = new NestHandlerCodec(this.config)
this.handler = new StandardHandler(this.codec, this.config)
this.toNestStandardLazyRequest = this.config.toNestStandardLazyRequest ?? ((req: ExpressRequest | FastifyRequest, res: ExpressResponse | FastifyReply) => {
this.toNestStandardLazyRequest = this.config.toNestStandardLazyRequest ?? ((req: ExpressRequest | FastifyRequest | NodeRequest, res: ExpressResponse | FastifyReply | NodeResponse) => {
const standardRequest: NestStandardLazyRequest = toStandardLazyRequest(
'raw' in req ? req.raw : req,
'raw' in res ? res.raw : res,
Expand Down Expand Up @@ -202,8 +203,8 @@ export class ImplementInterceptor implements NestInterceptor {
`)
}

const req: ExpressRequest | FastifyRequest = ctx.switchToHttp().getRequest()
const res: ExpressResponse | FastifyReply = ctx.switchToHttp().getResponse()
const req: ExpressRequest | FastifyRequest | NodeRequest = ctx.switchToHttp().getRequest()
const res: ExpressResponse | FastifyReply | NodeResponse = ctx.switchToHttp().getResponse()

const standardRequest = this.toNestStandardLazyRequest(req, res)

Expand Down Expand Up @@ -384,7 +385,7 @@ function toORPCOpenAPIParams(contract: AnyProcedureContract, params: NestStandar

// NullProtoObj prevents prototype injection when a param is named like `__proto__`
const orpcParams: Record<string, string> = new NullProtoObj()
// express use `path` while fastify use `*` for rest matching
// express and node use `path` while fastify uses `*` for rest matching
const restKey = Object.hasOwn(params, '*') ? '*' : 'path'

for (const [key, value] of Object.entries(params)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/nest/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export type ORPCModuleConfig
& {
/**
* Customize how to convert NestJS `req` and `res` to {@link NestStandardLazyRequest}.
* You might need to define this if you are not using express or fastify adapters.
* You might need to define this if you are not using the express, fastify or node adapters.
*/
toNestStandardLazyRequest?: ((req: any, res: any) => NestStandardLazyRequest) | undefined

Expand Down
8 changes: 4 additions & 4 deletions playgrounds/nest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
},
"devDependencies": {
"@nestjs/cli": "^12.0.8",
"@nestjs/common": "^12.1.3",
"@nestjs/core": "^12.1.3",
"@nestjs/platform-express": "^12.1.3",
"@nestjs/common": "^12.3.2",
"@nestjs/core": "^12.3.2",
"@nestjs/platform-express": "^12.3.2",
"@nestjs/schematics": "^12.0.7",
"@nestjs/testing": "^12.1.3",
"@nestjs/testing": "^12.3.2",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.223.0",
"@opentelemetry/instrumentation": "^0.223.0",
Expand Down
Loading
Loading