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
14 changes: 10 additions & 4 deletions apps/content/docs/integrations/nest.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,16 +279,22 @@ Configure how [event iterators](/docs/event-iterator) are streamed to the client
export class AppModule {}
```

### `toStandardLazyRequest` option
### `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. For details, see [Standard Server](http://standardserver.dev/).
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](http://standardserver.dev/).

```ts
import { NestStandardLazyRequest } from '@orpc/nest'
Comment thread
dinwwwh marked this conversation as resolved.
import { toStandardLazyRequest } from '@standardserver/fetch'

@Module({
imports: [
ORPCModule.forRoot({
toStandardLazyRequest: (req, res) => {
// your custom implementation
toNestStandardLazyRequest: (req, res) => {
// example Hono platform support
const standardRequest: NestStandardLazyRequest = toStandardLazyRequest(req.raw)
standardRequest.params = req.params
return standardRequest
},
Comment thread
dinwwwh marked this conversation as resolved.
}),
],
Expand Down
29 changes: 18 additions & 11 deletions packages/nest/src/implement.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'
import type { StandardLazyRequest } from '@standardserver/core'
import type { Request as ExpressRequest } from 'express'
import type { FastifyReply } from 'fastify'
import type { NestStandardLazyRequest } from './module'
import { Buffer } from 'node:buffer'
import FastifyCookie from '@fastify/cookie'
import { Controller, HttpException, Req, Res, StreamableFile } from '@nestjs/common'
Expand Down Expand Up @@ -907,9 +907,10 @@ describe('compatibility', () => {
expect(res.status).toBe(200)
})

it('can custom request parser with toStandardLazyRequest option', async () => {
it('can custom request parser with toNestStandardLazyRequest option', async () => {
const contract = oc.meta(openapi({
path: '/parser',
path: '/parser/{param}',
inputStructure: 'detailed',
}))

const handler = vi.fn(({ input }) => input)
Expand All @@ -922,33 +923,39 @@ describe('compatibility', () => {
}
}

const toStandardLazyRequest = vi.fn(() => ({
const toNestStandardLazyRequest = vi.fn(() => ({
url: '/test',
method: 'POST',
headers: {},
resolveBody: async () => '__OVERRIDED__',
} satisfies StandardLazyRequest))
params: { param: '__PARAM__' },
} satisfies NestStandardLazyRequest))

const moduleRef = await Test.createTestingModule({
controllers: [ImplController],
imports: [
ORPCModule.forRoot({
toStandardLazyRequest,
toNestStandardLazyRequest,
}),
],
}).compile()

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

const res = await supertest(app.getHttpServer()).post('/parser')
const res = await supertest(app.getHttpServer()).post('/parser/value')

expect(res.statusCode).toEqual(200)
expect(res.body).toEqual('__OVERRIDED__')
expect(res.body).toMatchObject({
body: '__OVERRIDED__',
params: {
param: '__PARAM__',
},
})

expect(toStandardLazyRequest).toHaveBeenCalledTimes(1)
expect(toStandardLazyRequest).toHaveBeenCalledWith(
expect.objectContaining({ url: '/parser' }),
expect(toNestStandardLazyRequest).toHaveBeenCalledTimes(1)
expect(toNestStandardLazyRequest).toHaveBeenCalledWith(
expect.objectContaining({ url: '/parser/value' }),
expect.objectContaining({ end: expect.any(Function) }),
)
})
Expand Down
18 changes: 9 additions & 9 deletions packages/nest/src/implement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { StandardBodyHint } from '@standardserver/core'
import type { Request as ExpressRequest, Response as ExpressResponse } from 'express'
import type { FastifyReply, FastifyRequest } from 'fastify'
import type { Observable } from 'rxjs'
import type { ORPCModuleConfig } from './module'
import type { NestStandardLazyRequest, ORPCModuleConfig } from './module'
import { Readable } from 'node:stream'
import { applyDecorators, Delete, Get, Head, HttpCode, HttpException, Inject, Injectable, Optional, Options, Patch, Post, Put, StreamableFile, UseInterceptors } from '@nestjs/common'
import { HttpAdapterHost } from '@nestjs/core'
Expand Down Expand Up @@ -94,7 +94,7 @@ export function Implement<T extends RouterContract>(
export class ImplementInterceptor implements NestInterceptor {
private readonly config: ORPCModuleConfig
private readonly codec: OpenAPIHandlerCodecCore<DefaultInitialContext>
private readonly toStandardLazyRequest: Exclude<ORPCModuleConfig['toStandardLazyRequest'], undefined>
private readonly toNestStandardLazyRequest: Exclude<ORPCModuleConfig['toNestStandardLazyRequest'], undefined>
private readonly httpAdapterHost: HttpAdapterHost

constructor(
Expand All @@ -106,8 +106,8 @@ export class ImplementInterceptor implements NestInterceptor {
this.httpAdapterHost = httpAdapterHost

this.codec = new OpenAPIHandlerCodecCore(this.config)
this.toStandardLazyRequest = this.config.toStandardLazyRequest ?? ((req: ExpressRequest | FastifyRequest, res: ExpressResponse | FastifyReply) => {
const standardRequest = toStandardLazyRequest(
this.toNestStandardLazyRequest = this.config.toNestStandardLazyRequest ?? ((req: ExpressRequest | FastifyRequest, res: ExpressResponse | FastifyReply) => {
const standardRequest: NestStandardLazyRequest = toStandardLazyRequest(
'raw' in req ? req.raw : req,
'raw' in res ? res.raw : res,
)
Expand All @@ -117,6 +117,8 @@ export class ImplementInterceptor implements NestInterceptor {
standardRequest.resolveBody = () => Promise.resolve(req.body)
}

standardRequest.params = req.params as NestStandardLazyRequest['params']

return standardRequest
})
}
Expand All @@ -135,15 +137,15 @@ export class ImplementInterceptor implements NestInterceptor {
const req: ExpressRequest | FastifyRequest = ctx.switchToHttp().getRequest()
const res: ExpressResponse | FastifyReply = ctx.switchToHttp().getResponse()

const standardRequest = this.toStandardLazyRequest(req, res)
const standardRequest = this.toNestStandardLazyRequest(req, res)

const handler = new StandardHandler({
resolveProcedure: () => Promise.resolve({
path: getPathMeta(procedure) ?? [],
procedure,
decodeInput: () => this.codec.decodeInput({
procedure,
params: toORPCOpenAPIParams(procedure, req.params as NestParams),
params: toORPCOpenAPIParams(procedure, standardRequest.params),
}, standardRequest),
Comment on lines +140 to 149

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Path params now depend entirely on the custom toNestStandardLazyRequest populating .params — no fallback to req.params.

Previously decodeInput read req.params directly regardless of any custom conversion hook. Now it only reads standardRequest.params, which is optional and not set by the default value unless the default converter runs. Anyone with a pre-existing custom toStandardLazyRequest implementation who mechanically renames it to toNestStandardLazyRequest (required to compile) will silently lose OpenAPI dynamic-path-param decoding unless they also remember to populate .params — this only surfaces later as an input-validation failure, not a compile error.

Consider falling back to the raw request params to preserve backward compatibility for existing custom implementations:

🛡️ Suggested fallback
             decodeInput: () => this.codec.decodeInput({
               procedure,
-              params: toORPCOpenAPIParams(procedure, standardRequest.params),
+              params: toORPCOpenAPIParams(procedure, standardRequest.params ?? (req.params as NestStandardLazyRequest['params'])),
             }, standardRequest),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const standardRequest = this.toNestStandardLazyRequest(req, res)
const handler = new StandardHandler({
resolveProcedure: () => Promise.resolve({
path: getPathMeta(procedure) ?? [],
procedure,
decodeInput: () => this.codec.decodeInput({
procedure,
params: toORPCOpenAPIParams(procedure, req.params as NestParams),
params: toORPCOpenAPIParams(procedure, standardRequest.params),
}, standardRequest),
const standardRequest = this.toNestStandardLazyRequest(req, res)
const handler = new StandardHandler({
resolveProcedure: () => Promise.resolve({
path: getPathMeta(procedure) ?? [],
procedure,
decodeInput: () => this.codec.decodeInput({
procedure,
params: toORPCOpenAPIParams(procedure, standardRequest.params ?? (req.params as NestStandardLazyRequest['params'])),
}, standardRequest),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nest/src/implement.ts` around lines 140 - 149, The path-parameter
decoding in implement.ts now depends only on toNestStandardLazyRequest
populating standardRequest.params, which breaks existing custom converters that
only renamed to the new hook. Update the decodeInput flow in implement() so it
preserves backward compatibility by falling back to the raw request params when
standardRequest.params is absent, using the existing
toNestStandardLazyRequest/standardRequest path as the primary source and
req.params as the fallback.

}),
encodeError: this.codec.encodeError.bind(this.codec),
Expand Down Expand Up @@ -248,9 +250,7 @@ function flattenParamValue(value: undefined | string | string[]): undefined | st
return Array.isArray(value) ? value.join('/') : value
}

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

function toORPCOpenAPIParams(contract: AnyProcedureContract, params: NestParams | undefined): undefined | Record<string, string> {
function toORPCOpenAPIParams(contract: AnyProcedureContract, params: NestStandardLazyRequest['params']): undefined | Record<string, string> {
const meta = getOpenAPIMeta(contract)

/* c8 ignore start - there cases almost never happen only for type guard purpose */
Expand Down
13 changes: 10 additions & 3 deletions packages/nest/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,23 @@ import { ImplementInterceptor } from './implement'

export const ORPC_MODULE_CONFIG_SYMBOL = Symbol.for('ORPC_NEST_MODULE_CONFIG')

export interface NestStandardLazyRequest extends StandardLazyRequest {
/**
* Route parameters extracted from the request path.
*/
params?: undefined | Record<string, string | string[]>
}

export type ORPCModuleConfig
= & OpenAPIHandlerCodecCoreOptions<DefaultInitialContext>
& StandardHandlerOptions<DefaultInitialContext>
& (object extends DefaultInitialContext ? { context?: DefaultInitialContext } : { context: DefaultInitialContext })
& {
/**
* Customize how convert next.js req and res to StandardLazyRequest,
* You might need define this if you not using express or fastify adapters
* 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.
*/
toStandardLazyRequest?: undefined | ((req: any, res: any) => StandardLazyRequest)
toNestStandardLazyRequest?: undefined | ((req: any, res: any) => NestStandardLazyRequest)
Comment thread
dinwwwh marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Options for how to convert the Standard Response to a Nest Response (returning value), like event iterator options, etc.
Expand Down
Loading