Skip to content

Conversation

@Mathiasduc
Copy link

@Mathiasduc Mathiasduc commented Oct 29, 2025

This PR all the tested features of Nest to integrate better with orpc and it's feature.
For example:

  1. global interceptors can now modify the Orpc response.
  2. method interceptors declared after @implement can modify the response.
  3. guards that, for example, write on the req object will now be able to access the req and res object in the handler context. (was already possible with the context config if I understood this correctly? )
  4. Filter and especially Exception filters can now choose to catch the ORPC errors or declare the newly provided ORPCExceptionFilter to keep the same behaviour.

Basically everything that was trying to modify the response after the handler was failing due to the handler sending the response.
The handler now set headers, status code, return the proper body and let the middleware chain resolve and let Nest/Node send the response at the end.

Summary by CodeRabbit

  • New Features

    • Added ORPCExceptionFilter for standardized error response handling in NestJS applications.
    • Enhanced HTTP response utilities for Fastify and Node server integration with improved body/header normalization.
  • Documentation

    • Added "Error Handling" section to Nest integration guide with usage patterns and code examples.
  • Tests

    • Added comprehensive integration test suite covering Express and Fastify adapters with response type validation, middleware, guards, and compression.

@vercel
Copy link

vercel bot commented Oct 29, 2025

@Mathiasduc is attempting to deploy a commit to the unnoq-team Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Oct 29, 2025

Walkthrough

This PR enhances the Nest integration with standardized error handling and response utilities. It introduces ORPCExceptionFilter for converting ORPCError instances into oRPC-formatted responses, adds utility functions for setting HTTP responses across Fastify and Node adapters, refactors the request handler implementation to separate error concerns, and includes comprehensive test coverage for response types and Nest features.

Changes

Cohort / File(s) Summary
Error Handling & Filtering
packages/nest/src/filters/orpc-exception.filter.ts, packages/nest/README.md
New ORPCExceptionFilter class that catches ORPCError exceptions and encodes them into standardized oRPC error responses via Fastify or Node servers; documentation added with usage patterns for global, module-level, or default Nest handling.
Response Utilities
packages/nest/src/utils.ts
Added three helper functions: setStandardFastifyResponse and setStandardNodeResponse for dispatching standardized responses, plus getStandardHttpBodyAndHeaders for normalizing various body types (Blob, FormData, URLSearchParams, async iterators, strings) with appropriate headers.
Core Implementation
packages/nest/src/implement.ts
Refactored to separate decoding and encoding error handling into distinct try/catch blocks; replaced inline logic with explicit client calls; now delegates non-ORPC decoding errors to exception filters and uses new response setter functions.
Public API Export
packages/nest/src/index.ts
Added re-export of ORPCExceptionFilter and related exports from ./filters/orpc-exception.filter.
Dependencies
packages/nest/package.json
Added devDependencies: @fastify/compress, @types/compression, @types/node, @types/supertest, and compression.
Test Infrastructure
packages/nest/src/implement.test.ts, packages/nest/tests/nest-features.test.ts, packages/nest/tests/response-types.test.ts
Updated existing tests to use new ORPCExceptionFilter and setStandardResponse naming; added comprehensive new test suites covering response type handling, Nest features (interceptors, guards, pipes, middleware), compression, and multi-adapter compatibility (Express and Fastify).
Documentation
packages/standard-server-node/src/body.ts
Minor JSDoc update clarifying that headers parameter may be mutated by the function.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant NestApp as NestJS App
    participant Filter as ORPCExceptionFilter
    participant Encoder as StandardOpenAPICodec
    participant Server as StandardServerNode/Fastify
    participant Response as HTTP Response

    Client->>NestApp: Send ORPC Request
    NestApp->>NestApp: Decode Input (fails)
    NestApp->>Filter: Throw ORPCError
    Filter->>Encoder: Encode ORPCError
    Encoder-->>Filter: Encoded Error Response
    Filter->>Server: Send Standard Response
    Server->>Response: Set Status & Headers
    Server->>Response: Write Body
    Response-->>Client: HTTP Response
Loading
sequenceDiagram
    participant Handler as ORPC Handler
    participant Utils as setStandardNodeResponse
    participant Parser as getStandardHttpBodyAndHeaders
    participant HTTP as Node HTTP Response

    Handler->>Handler: Execute & Get Result
    Handler->>Utils: setStandardNodeResponse(res, standardResponse)
    Utils->>Parser: getStandardHttpBodyAndHeaders(body, headers)
    alt Body is Blob
        Parser->>Parser: Create Readable Stream
    else Body is FormData
        Parser->>Parser: Convert via Response API
    else Body is AsyncIterator
        Parser->>Parser: Format as Event Stream
    else Other
        Parser->>Parser: Stringify & Format
    end
    Parser-->>Utils: { body, headers }
    Utils->>HTTP: Set Status Code
    Utils->>HTTP: Set Headers
    Utils->>HTTP: Write/Pipe Body
    HTTP-->>Handler: Complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–25 minutes

  • Exception filter logic: Review error encoding, Fastify vs. Node detection, and proper exception handling flow in orpc-exception.filter.ts.
  • Response utility complexity: Examine body normalization logic in getStandardHttpBodyAndHeaders for edge cases across multiple content types (Blob, FormData, URLSearchParams, streams).
  • Implementation refactoring: Verify error-handling separation in implement.ts, ensure decoding/encoding errors are properly routed to filters or handled inline.
  • Test coverage breadth: New test files span multiple adapter types and Nest features; verify assertions align with expected error/response behavior.

Possibly related PRs

Poem

🐰 Errors caught in filters fine,
Responses set with helpers divine,
Fastify, Node—both dance in line,
Nest integration—oh how they shine!
thump thump

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(nest): Change the nest integration to support Nest features that were competing with Orpc' accurately reflects the main objective of the changeset. The modifications across multiple files enable better interoperability between Nest platform features (global interceptors, guards, filters, compression) and the oRPC integration by changing how responses are handled and sent. The title captures the essence of this significant architectural shift without being overly specific about implementation details.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Mathiasduc
Copy link
Author

Mathiasduc commented Oct 29, 2025

Hi @unnoq This is the PR we were talking about in #992 .
It is , I feel like, a big departure from the previous implementation and other integrations but I think it is the most standard and future proof implementation of this integration.
It will allow teams, like us, to better migrate to Orpc from an existing Nest app.
Current implementation breaks a lot of feature that most Nest app probably use.
This is in draft because I'm not satisfied with the testing.
It lacks testing for the new 'standard' functions and more in depth testing for mixing Nest and Orpc feature.
But I'm curious if that approach is ok with you and if I can keep iterating and keep at polishing it.

@Mathiasduc Mathiasduc force-pushed the feat/server/nest/add-support-for-nestjs-features branch from 6892585 to 8366106 Compare October 29, 2025 12:11
Copy link
Owner

@unnoq unnoq left a comment

Choose a reason for hiding this comment

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

I still don’t understand why we need to use return just to increase compatibility with NestJS. Modifying the response body is an anti-pattern and makes it incompatible with the generated openapi spec. Why not modify the headers instead? We can already change headers without requiring a return.


const client = createProcedureClient(procedure, {
...this.config,
context: contextWithRequest,
Copy link
Owner

@unnoq unnoq Oct 29, 2025

Choose a reason for hiding this comment

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

This approach is not typesafe. While implement expects an empty context {}, this can lead to problems. For instance, a middleware might expect a context like { request?: number | undefined }. Because an empty object {} doesn't conflict can satisfy middleware's dependent-context so we can .use when implement. But in this case request is req -> middleware can run failed because middleware expect request is undefined or number

* @param headers - WARNING: The headers can be mutated by the function and may affect the original headers.
* @param options
*/
export function toResponseBody(
Copy link
Owner

Choose a reason for hiding this comment

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

I don't think this package needs these APIs. I believe we should write it inside the nest package instead. We can implement some of them as signal functions

Copy link
Author

Choose a reason for hiding this comment

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

I'll try to not need to define this function since it's so similar to toNodeHttpBody. Adding an option to stringify the body or not should be enough.

We can implement some of them as signal functions

I did not understand what you meant by that.

Copy link
Owner

Choose a reason for hiding this comment

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

I mean you can write a signal function in nest package instead.

import * as StandardServerFastify from '@orpc/standard-server-fastify'
import * as StandardServerNode from '@orpc/standard-server-node'

const codec = new StandardOpenAPICodec(
Copy link
Owner

Choose a reason for hiding this comment

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

I'm looking for a way to make all codecs extendable. Specifically, I'd like to know if it's possible to add a custom serializer by passing it into the main configuration, similar to the method described for extending native data types.

// Set status and headers
if ('raw' in res) {
await StandardServerFastify.sendStandardResponse(res, standardResponse, this.config)
return StandardServerFastify.setStandardResponse(res as FastifyReply, standardResponse, this.config)
Copy link
Owner

Choose a reason for hiding this comment

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

Make sure you tests there cases:

  • undefined in this case expect body is empty + no content-type is set
  • json data in this case expect return json data (not stringify) + content-type is json
  • file/blob in this case expect streaming response + content-type = file.type

@Mathiasduc Mathiasduc changed the title feat(server): Change the nest integration to support Nest features that were competing with Orpc feat(nest): Change the nest integration to support Nest features that were competing with Orpc Oct 29, 2025
@codecov
Copy link

codecov bot commented Oct 29, 2025

Codecov Report

❌ Patch coverage is 92.00000% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/nest/src/implement.ts 74.19% 8 Missing ⚠️
packages/nest/src/filters/orpc-exception.filter.ts 93.54% 2 Missing ⚠️
packages/nest/src/utils.ts 97.70% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@pkg-pr-new
Copy link

pkg-pr-new bot commented Oct 29, 2025

More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@1149

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@1149

@orpc/client

npm i https://pkg.pr.new/@orpc/client@1149

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@1149

@orpc/experimental-durable-iterator

npm i https://pkg.pr.new/@orpc/experimental-durable-iterator@1149

@orpc/hey-api

npm i https://pkg.pr.new/@orpc/hey-api@1149

@orpc/interop

npm i https://pkg.pr.new/@orpc/interop@1149

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@1149

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@1149

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@1149

@orpc/openapi-client

npm i https://pkg.pr.new/@orpc/openapi-client@1149

@orpc/otel

npm i https://pkg.pr.new/@orpc/otel@1149

@orpc/experimental-publisher

npm i https://pkg.pr.new/@orpc/experimental-publisher@1149

@orpc/react

npm i https://pkg.pr.new/@orpc/react@1149

@orpc/react-query

npm i https://pkg.pr.new/@orpc/react-query@1149

@orpc/experimental-react-swr

npm i https://pkg.pr.new/@orpc/experimental-react-swr@1149

@orpc/server

npm i https://pkg.pr.new/@orpc/server@1149

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@1149

@orpc/solid-query

npm i https://pkg.pr.new/@orpc/solid-query@1149

@orpc/standard-server

npm i https://pkg.pr.new/@orpc/standard-server@1149

@orpc/standard-server-aws-lambda

npm i https://pkg.pr.new/@orpc/standard-server-aws-lambda@1149

@orpc/standard-server-fastify

npm i https://pkg.pr.new/@orpc/standard-server-fastify@1149

@orpc/standard-server-fetch

npm i https://pkg.pr.new/@orpc/standard-server-fetch@1149

@orpc/standard-server-node

npm i https://pkg.pr.new/@orpc/standard-server-node@1149

@orpc/standard-server-peer

npm i https://pkg.pr.new/@orpc/standard-server-peer@1149

@orpc/svelte-query

npm i https://pkg.pr.new/@orpc/svelte-query@1149

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@1149

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@1149

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@1149

@orpc/vue-colada

npm i https://pkg.pr.new/@orpc/vue-colada@1149

@orpc/vue-query

npm i https://pkg.pr.new/@orpc/vue-query@1149

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@1149

commit: c82d19b

@Mathiasduc
Copy link
Author

Mathiasduc commented Oct 29, 2025

We can already change headers without requiring a return.

Not from inside a Nest/Express/Fastify middleware if the response has already been sent by Orpc

Modifying the response body is an anti-pattern and makes it incompatible with the generated openapi spec.

It is but, I feel like, this should not be a concern of this integration. There is legitimate reasons to modifying a response defined by Orpc and ways to do it so it will not break contract/specs.
For example:

  • If I'm using the standard compression middleware libs in Nest (same apply for the express and fastify integrations as they are today)
  • I want to add a header based on the body
  • I want to encrypt the response body

This features exist in Orpc eco-system, with plugin or external packages, and users can also implement them at the ORPC level (orpc's middleware/interceptors) but why not make it possible to delegate this features to the platform being integrated with, Nest in this case.
It will ease the progressive migration to orpc, thanks to not having to replicate, reimplement and maintain every unsupported feature.

But, indeed, it also let the door open to the users to shoot themselves in the foot and break their own contract and specs.
Should this integration (and other integrations too for that matter) close the door to some features of the platform being integrated with ? Users of this platform are using Nest/express/fastify middlewares as they are specified, designed and documented and will expect them to work with ORPC.
We do not think that limiting platform feature compatibility in the name of 'safety', is an acceptable trade off.

@Mathiasduc
Copy link
Author

We can move this discussion in an issue if you prefer

@unnoq
Copy link
Owner

unnoq commented Oct 29, 2025

I feel like, this should not be a concern of this integration.

Reasonable, and I agreed - I just don't want mess thing up by introducing new apis, if you do this good enough I have no reason to reject.

I want to encrypt the response body

I believe without return encrypt still work, because under the hook its only express middleware or fastify plugin.

@unnoq
Copy link
Owner

unnoq commented Oct 30, 2025

If I'm using the [standard compression](https://docs.nestjs.com/techniques/compression) middleware libraries in Nest (the same applies to the express and fastify integrations as they exist today):

  • I want to add a header based on the body
  • I want to encrypt the response body

After thinking about it more, can you provide real examples or popular npm packages that depend on this? Personally, I believe that under the hood they should be Express middleware or Fastify plugins - so I don't think they need return body here.

Also, regarding your implementation, we can't always return body because streaming may need to be sent manually, and this inconsistent behavior is something we should avoid as well.

}
})

resBody.once('error', error => res.destroy(error))
Copy link
Owner

Choose a reason for hiding this comment

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

In Node.js we manually send streaming responses, but in Fastify you return the stream - this creates inconsistent behavior.

body: StandardBody,
headers: StandardHeaders,
options: ToNodeHttpBodyOptions = {},
options: ToNodeHttpBodyOptions = { shouldStringifyBody: true },
Copy link
Owner

Choose a reason for hiding this comment

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

You should create other function or intercept before call toNodeHttpBody, this option is not related to this util + default option like this is not safe (we can easily accidentally disable strintify).

Copy link
Author

Choose a reason for hiding this comment

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

Sure, the default is the existing behaviour so it should not be an issue but I understand the worry. I will extract this change in another function. I'm also finishing on writing a test suite for all the core request type that need to be supported (basically everything in this file.)
I would also like to create another PR with all these tests to demonstrate the limitation of the current implementation vs this one.

@Mathiasduc
Copy link
Author

Mathiasduc commented Oct 31, 2025

Hi, I created another PR that add this new tests against the current main implementation.

I believe without return encrypt still work, because under the hook its only express middleware or fastify plugin.

You were right, I'm not sure how but this indeed work, even with current implementation.
How can a middleware encode the body when it is directly sent from the current Interceptor implementation, I'm not sure. I will dig further to understand.

It leave the rest of the failing tests that cannot be supported when sending the response at the Orpc level.
I can add more test to better ensure no regression with the returning body approach if you are still willing to consider this changes that will improve this integration.

@Mathiasduc
Copy link
Author

Mathiasduc commented Oct 31, 2025

So the compression case do indeed work because the Node/Express version actually patches the res.send method ! 🤯
The fastify version use a hook to intercept the body:

fastify.addHook('onSend', async (request, reply, payload) => {
  // Check if compression should be applied
  if (!shouldCompress(request, reply)) {
    return payload
  }
  
  // Modify content-encoding header
  reply.header('content-encoding', 'gzip')
  reply.removeHeader('content-length')
  
  // Return compressed payload
  if (isStream(payload)) {
    return payload.pipe(createGzip())
  } else {
    return compress(payload)
  }
})

This is some wild monkeypatching but it works

@unnoq
Copy link
Owner

unnoq commented Nov 3, 2025

I can add more test to better ensure no regression with the returning body approach if you are still willing to consider this changes that will improve this integration.

I'm hesitant to add Nest.js-specific handling because I'm not familiar with the framework and haven't used it before. I need a concrete reason for this change - based on my understanding, return isn't necessary for ecosystem compatibility. The Next.js docs also show examples of manually sending responses when needed.

Could you provide a real-world example or point to npm packages that specifically require return for this to work? That would help me evaluate this approach.

@Mathiasduc Mathiasduc force-pushed the feat/server/nest/add-support-for-nestjs-features branch from 21850e3 to f56f7a0 Compare November 3, 2025 08:29
@Mathiasduc Mathiasduc marked this pull request as ready for review November 3, 2025 08:29
@dosubot dosubot bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Nov 3, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
packages/standard-server-node/src/body.ts (1)

57-57: Consider more explicit documentation about header mutation.

While "may affect" is clearer than the previous wording, it's still somewhat ambiguous. Consider being more explicit about when and how headers are mutated.

Apply this diff for clearer documentation:

-  * @param headers - WARNING: The headers can be mutated by the function and may affect the original headers.
+  * @param headers - WARNING: This object is mutated directly. Content-type and content-disposition headers are deleted and then repopulated based on the body type.
packages/nest/src/filters/orpc-exception.filter.ts (2)

11-16: Hardcoded codec limits extensibility.

The module-level codec instance prevents users from customizing error serialization. A past review comment requested making codecs extendable through configuration.

Consider accepting the codec as a constructor parameter or configuration option:

+import type { StandardCodec } from '@orpc/standard-server'
+
-const codec = new StandardOpenAPICodec(
+const defaultCodec = new StandardOpenAPICodec(
   new StandardOpenAPISerializer(
     new StandardOpenAPIJsonSerializer(),
     new StandardBracketNotationSerializer(),
   ),
 )

 @Catch(ORPCError)
 @Injectable()
 export class ORPCExceptionFilter implements ExceptionFilter {
   constructor(
-    private config?: StandardServerNode.SendStandardResponseOptions | undefined,
+    private options?: {
+      codec?: StandardCodec
+      responseOptions?: StandardServerNode.SendStandardResponseOptions
+    },
   ) {}

   async catch(exception: ORPCError<any, any>, host: ArgumentsHost) {
     const ctx = host.switchToHttp()
     const res = ctx.getResponse<Response | FastifyReply>()
-    const standardResponse = codec.encodeError(exception)
+    const codec = this.options?.codec ?? defaultCodec
+    const standardResponse = codec.encodeError(exception)
     // Send the response directly with proper status and headers
     const isFastify = 'raw' in res
     if (isFastify) {
-      await StandardServerFastify.sendStandardResponse(res as FastifyReply, standardResponse, this.config)
+      await StandardServerFastify.sendStandardResponse(res as FastifyReply, standardResponse, this.options?.responseOptions)
     }
     else {
-      await StandardServerNode.sendStandardResponse(res as Response, standardResponse, this.config)
+      await StandardServerNode.sendStandardResponse(res as Response, standardResponse, this.options?.responseOptions)
     }
   }
 }

This would allow users to provide custom serializers as mentioned in the extending native data types documentation.

Based on past review comments.


56-62: Consider extracting response dispatch logic.

The Fastify/Node detection and response dispatch pattern is duplicated in implement.ts (lines 169-174). Consider extracting this into a shared utility function.

Extract to a helper in utils.ts:

export async function dispatchStandardResponse(
  res: Response | FastifyReply,
  standardResponse: StandardResponse,
  options?: StandardServerNode.SendStandardResponseOptions,
): Promise<void> {
  const isFastify = 'raw' in res
  if (isFastify) {
    await StandardServerFastify.sendStandardResponse(res as FastifyReply, standardResponse, options)
  } else {
    await StandardServerNode.sendStandardResponse(res as Response, standardResponse, options)
  }
}

Then both the filter and interceptor can use this shared logic.

packages/nest/src/response-types.test.ts (1)

425-460: Loosen SSE assertions to match the spec.

text/event-stream responses frequently append ; charset=utf-8, and the field grammar allows optional whitespace after the colon. The current .expect('Content-Type', 'text/event-stream') and /data: (.+)/ patterns will fail or miss events even though oRPC is behaving correctly. Switch to something like .expect('Content-Type', /text\/event-stream/) and /^data:\s*(.+)$/m (or similar) so we acknowledge valid MIME parameters and the optional space defined in the SSE ABNF.

packages/nest/src/nest-features.test.ts (1)

400-575: Reset the spy between tests.

sendStandardResponseSpy is defined once for the whole suite; without a mockClear()/mockRestore() in the lifecycle hooks, earlier expectations can taint later ones when this file (or others) add more tests. Clearing it in beforeEach/afterEach keeps the “not called” assertions deterministic.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c38c37 and f56f7a0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • packages/nest/README.md (1 hunks)
  • packages/nest/package.json (1 hunks)
  • packages/nest/src/filters/orpc-exception.filter.ts (1 hunks)
  • packages/nest/src/implement.test.ts (6 hunks)
  • packages/nest/src/implement.ts (2 hunks)
  • packages/nest/src/index.ts (1 hunks)
  • packages/nest/src/nest-features.test.ts (1 hunks)
  • packages/nest/src/response-types.test.ts (1 hunks)
  • packages/nest/src/utils.ts (2 hunks)
  • packages/standard-server-node/src/body.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
packages/nest/src/filters/orpc-exception.filter.ts (4)
packages/openapi/src/adapters/standard/openapi-codec.ts (1)
  • StandardOpenAPICodec (23-132)
packages/nest/src/implement.ts (1)
  • Injectable (101-178)
packages/standard-server-fastify/src/response.ts (1)
  • SendStandardResponseOptions (6-6)
packages/standard-server-node/src/response.ts (1)
  • SendStandardResponseOptions (6-6)
packages/nest/src/implement.ts (5)
packages/server/src/procedure-client.ts (1)
  • createProcedureClient (72-165)
packages/standard-server/src/types.ts (1)
  • StandardResponse (34-41)
packages/client/src/error.ts (1)
  • ORPCError (112-169)
packages/standard-server/src/utils.ts (1)
  • flattenHeader (51-61)
packages/nest/src/utils.ts (2)
  • setStandardFastifyResponse (65-80)
  • setStandardNodeResponse (82-119)
packages/nest/src/nest-features.test.ts (3)
packages/contract/src/builder.ts (1)
  • oc (189-198)
packages/nest/src/implement.ts (2)
  • Implement (38-90)
  • Injectable (101-178)
packages/nest/src/filters/orpc-exception.filter.ts (1)
  • Catch (44-64)
packages/nest/src/response-types.test.ts (3)
packages/contract/src/builder.ts (1)
  • oc (189-198)
packages/nest/src/implement.ts (1)
  • Implement (38-90)
packages/nest/src/nest-features.test.ts (1)
  • data (252-261)
packages/nest/src/utils.ts (6)
packages/standard-server/src/types.ts (3)
  • StandardResponse (34-41)
  • StandardBody (5-11)
  • StandardHeaders (1-3)
packages/standard-server-node/src/body.ts (1)
  • ToNodeHttpBodyOptions (53-53)
packages/standard-server-node/src/types.ts (1)
  • NodeHttpResponse (17-17)
packages/standard-server/src/utils.ts (2)
  • flattenHeader (51-61)
  • generateContentDisposition (4-13)
packages/shared/src/iterator.ts (1)
  • isAsyncIteratorObject (6-12)
packages/shared/src/json.ts (1)
  • stringifyJSON (9-12)
🔇 Additional comments (10)
packages/nest/package.json (1)

64-77: LGTM!

The new devDependencies are appropriate for testing compression features with both Express and Fastify adapters.

packages/nest/src/utils.ts (2)

65-80: Return semantics change requires clarification.

The function returns the body for Fastify replies (line 78) instead of sending the response directly. This is a departure from the previous sendStandardResponse approach.

Based on the PR discussion, unnoq raised concerns about:

  1. Streaming cases where handlers cannot always return body
  2. Inconsistent behavior between streaming and non-streaming responses

Please clarify:

  • How does this work with streaming responses (async iterators)?
  • What happens to the returned body - does Nest automatically send it?
  • Does this approach work consistently across all StandardBody types (undefined, Blob, FormData, URLSearchParams, async iterators, JSON)?

Consider adding integration tests that verify all StandardBody types work correctly with the return approach, especially streaming cases.


161-167: The "double stringify" terminology is misleading—clarify what's actually happening.

The code calls stringifyJSON once, not twice. When a string body is passed, JSON.stringify correctly encodes it by adding quotes (e.g., "hello" becomes "\"hello\""), which is necessary for valid JSON. However, the comment's wording suggests a workaround or unusual behavior, which is confusing. The implementation is correct, but consider:

  • Update the comment to clarify that this is standard JSON encoding of string values, not a workaround
  • Add a test case for string bodies to document and validate this behavior
  • Remove references to "double stringify," which is inaccurate terminology
packages/nest/src/implement.ts (2)

128-167: Improved error handling structure.

The explicit separation of decoding, execution, and encoding phases with targeted error handling is clear and maintainable. Each phase appropriately wraps non-ORPC errors into ORPCError instances.

The approach allows NestJS exception filters to handle all errors uniformly while providing clear error messages for each phase.


169-174: No action required—comprehensive tests confirm the return-based response handling approach is working correctly.

Verification found that tests are parameterized to run with both Express and Fastify adapters, which means both setStandardFastifyResponse and setStandardNodeResponse branches are exercised. Test coverage includes all requested StandardBody type variations:

  • Empty/undefined responses
  • JSON/object responses
  • URLSearchParams with proper content-type headers
  • FormData with multipart handling
  • Blob responses
  • AsyncIterable/streaming (SSE) with text/event-stream content-type

The parameterized test structure ensures the 'raw' in res check at implement.ts line 169 correctly routes to both response handlers across Express and Fastify, validating the return-based approach works consistently for all response types.

packages/nest/src/index.ts (1)

1-1: LGTM!

The export appropriately exposes the new ORPCExceptionFilter for public use.

packages/nest/README.md (1)

71-98: Clear documentation of the new error handling approach.

The documentation effectively explains the optional nature of ORPCExceptionFilter and provides practical examples for different registration methods.

Consider adding a note about customizing error serialization once the codec is made configurable (as suggested in the review of orpc-exception.filter.ts).

packages/nest/src/implement.test.ts (3)

20-21: Good test instrumentation for the response handling change.

Adding setStandardResponseSpy alongside the existing sendStandardResponseSpy allows verifying the new response dispatch mechanism.


140-147: Appropriate integration of exception filter in tests.

The test setup correctly registers ORPCExceptionFilter via APP_FILTER provider, which is necessary for the pong test that throws errors.

This ensures consistent error handling behavior across test cases.


427-430: Tests for all StandardBody types are comprehensive and complete.

Verification confirms that response-types.test.ts exists and provides adequate coverage for all requested body types: undefined (7 tests), Blob/File (9 tests), FormData (4 tests), URLSearchParams (6 tests), and async iterators/event streaming (11 tests). The setStandardResponseSpy references in implement.test.ts are properly validated against these integration tests.

Comment on lines +121 to +170
export function getStandardHttpBodyAndHeaders(
body: StandardBody,
headers: StandardHeaders,
options: ToNodeHttpBodyOptions = {},
): { body: Readable | undefined | string, headers: StandardHeaders } {
const newHeaders: StandardHeaders = { ...omit(headers, ['content-disposition', 'content-type']) }

if (body === undefined) {
return { body: undefined, headers: newHeaders }
}

if (body instanceof Blob) {
newHeaders['content-type'] = body.type
newHeaders['content-length'] = body.size.toString()
const currentContentDisposition = flattenHeader(headers['content-disposition'])
newHeaders['content-disposition'] = currentContentDisposition ?? generateContentDisposition(body instanceof File ? body.name : 'blob')

return { body: Readable.fromWeb(body.stream()), headers: newHeaders }
}

if (body instanceof FormData) {
const response = new Response(body)
newHeaders['content-type'] = response.headers.get('content-type')!
// The FormData type inferred is from React and not NodeJS, so we need to cast it
return { body: Readable.fromWeb(response.body as any), headers: newHeaders }
}

if (body instanceof URLSearchParams) {
newHeaders['content-type'] = 'application/x-www-form-urlencoded'

return { body: body.toString(), headers: newHeaders }
}

if (isAsyncIteratorObject(body)) {
newHeaders['content-type'] = 'text/event-stream'

return { body: toEventStream(body, options), headers: newHeaders }
}

newHeaders['content-type'] = 'application/json'
// It seems like Nest/Node, in case of a string body, remove or alter the string if
// content type json is not set.
// We also need to "double" stringify it, else the string will be encoded as an Array
// This match the behavior of #toNodeHttpBody
if (typeof body === 'string') {
return { body: stringifyJSON(body), headers: newHeaders }
}

return { body: body as unknown as string, headers: newHeaders }
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Significant code duplication with toNodeHttpBody from standard-server-node.

The logic in getStandardHttpBodyAndHeaders closely mirrors toNodeHttpBody from packages/standard-server-node/src/body.ts (lines 60-104). This creates maintenance burden and potential for divergence.

Consider refactoring to reuse the existing toNodeHttpBody function rather than reimplementing the logic. Based on past review comments, you could:

  1. Extract the common logic into a shared helper that both functions can use
  2. Add an option to toNodeHttpBody to return headers separately instead of mutating them
  3. Call toNodeHttpBody and then process the result

Example approach:

export function getStandardHttpBodyAndHeaders(
  body: StandardBody,
  headers: StandardHeaders,
  options: ToNodeHttpBodyOptions = {},
): { body: Readable | undefined | string, headers: StandardHeaders } {
  // Clone headers to avoid mutation
  const newHeaders: StandardHeaders = { ...headers }
  
  // Reuse existing logic
  const processedBody = toNodeHttpBody(body, newHeaders, options)
  
  return { body: processedBody, headers: newHeaders }
}

This would eliminate the duplication while maintaining the desired behavior.

// We also need to "double" stringify it, else the string will be encoded as an Array
// This match the behavior of #toNodeHttpBody
if (typeof body === 'string') {
return { body: stringifyJSON(body), headers: newHeaders }
Copy link
Owner

Choose a reason for hiding this comment

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

another inconsistent behavior 🤕

Copy link
Author

Choose a reason for hiding this comment

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

Hi, I don't think so, that was the previous behaviour. I tested it

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f56f7a0 and c82d19b.

📒 Files selected for processing (2)
  • packages/nest/tests/nest-features.test.ts (1 hunks)
  • packages/nest/tests/response-types.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/nest/tests/nest-features.test.ts (3)
packages/contract/src/builder.ts (1)
  • oc (189-198)
packages/nest/src/implement.ts (1)
  • Injectable (101-178)
packages/nest/src/filters/orpc-exception.filter.ts (1)
  • Catch (44-64)
packages/nest/tests/response-types.test.ts (2)
packages/contract/src/builder.ts (1)
  • oc (189-198)
packages/nest/tests/nest-features.test.ts (1)
  • data (252-261)
🔇 Additional comments (9)
packages/nest/tests/response-types.test.ts (3)

14-112: Excellent documentation and comprehensive contract coverage.

The test contracts cover all ORPC standard body types as documented. The inline documentation provides clear context for the test purpose and references.


118-250: Well-structured controller implementations.

Each handler correctly implements its contract using the appropriate response type. The implementations demonstrate proper usage of ORPC features like withEventMeta for SSE streams and detailed output structures.


463-542: Proper SSE implementation with Last-Event-ID.

The event metadata tests correctly implement SSE reconnection semantics using the Last-Event-ID header. The use of GET requests without bodies is appropriate for SSE streams.

packages/nest/tests/nest-features.test.ts (6)

1-85: Good test setup with appropriate middleware imports.

The test file properly imports both Express and Fastify compression middleware for testing compression behavior across adapters. The contracts comprehensively cover different NestJS features (guards, pipes, filters, interceptors).


88-262: Well-structured controllers demonstrating NestJS features.

Each controller correctly demonstrates its respective NestJS feature:

  • Guards properly modify the request object (setting request.user)
  • Error controller throws HttpException for filter testing
  • Compression controller returns sufficient data (>1KB) to trigger compression

The guard implementation at lines 175-190 properly simulates JWT-style authentication by adding user data to the request, which is then accessible in the handler.


264-296: Global interceptor and filter correctly test PR objectives.

The implementations demonstrate the PR's goal of allowing NestJS middleware to modify ORPC responses:

  • GlobalLoggingInterceptor modifies both response body and headers
  • GlobalHttpExceptionFilter bypasses ORPC's exception handling to provide custom error formatting

These patterns align with the PR's objective to restore standard NestJS feature compatibility.


298-398: Proper module configuration with NestJS providers.

All test modules correctly configure their respective features using NestJS's provider system (APP_PIPE, APP_FILTER, APP_INTERCEPTOR). The compression middleware is properly applied using compression() for Express (line 396).


483-514: Excellent documentation of pipe behavior with ORPC.

The detailed comment (lines 483-497) provides valuable context about why NestJS global pipes don't transform ORPC handler inputs and offers clear alternatives. This prevents user confusion and documents expected behavior. The test correctly validates that pipes coexist with ORPC without causing errors.


545-608: Proper verification of exception filter precedence.

The test correctly verifies that when a global NestJS exception filter handles an error, ORPC's exception handling is bypassed (line 574). This demonstrates that the PR successfully allows standard NestJS filters to intercept errors before ORPC's error handling.

The compression test properly validates that middleware can access and compress the ORPC response body, which was a key objective of this PR.

Comment on lines +89 to +92
eventStream: oc.route({
path: '/event-stream',
method: 'GET',
}).input(z.object({ count: z.number().optional() }).optional()),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

GET request with body is non-standard and unreliable.

The eventStream contract defines a GET method with input body (lines 89-92), and the test attempts to send a body with GET requests (lines 427-428). While the test includes fallback logic to skip when the response is undefined, this approach is problematic:

  • GET requests with bodies violate HTTP semantics and have inconsistent support across servers, proxies, and clients.
  • The fallback makes the test non-deterministic—it may pass by skipping rather than validating functionality.

Consider one of these solutions:

Solution 1 (Recommended): Use query parameters for GET

 eventStream: oc.route({
   path: '/event-stream',
   method: 'GET',
 }).input(z.object({ count: z.number().optional() }).optional()),

Then modify the test to use query parameters:

-await request(app.getHttpServer())
-  .get('/event-stream')
-  .send({ count: 3 })
+await request(app.getHttpServer())
+  .get('/event-stream?count=3')

Solution 2: Change to POST method

 eventStream: oc.route({
   path: '/event-stream',
-  method: 'GET',
+  method: 'POST',
 }).input(z.object({ count: z.number().optional() }).optional()),

Also applies to: 205-215, 421-461

@Mathiasduc
Copy link
Author

I can add more test to better ensure no regression with the returning body approach if you are still willing to consider this changes that will improve this integration.

I'm hesitant to add Nest.js-specific handling because I'm not familiar with the framework and haven't used it before. I need a concrete reason for this change - based on my understanding, return isn't necessary for ecosystem compatibility. The Next.js docs also show examples of manually sending responses when needed.

Could you provide a real-world example or point to npm packages that specifically require return for this to work? That would help me evaluate this approach.

I'm working on our integration. That should inform some real world issues that remains.
I will come back to this PR to update you and finish

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants