fix(nest): apply method-level decorators to router-contract @Implement routes - #1776
Conversation
…methods NestJS method-level enhancers (@UseGuards, @UsePipes, @UseFilters, @SetMetadata, ...) store metadata on the method's function object, but router-contract @implement registers synthesized functions as routes, so guards on the decorated method silently never ran. Synthesized methods now inherit from the original method via the prototype chain, which NestJS's Reflect.getMetadata lookups traverse.
…ement Decorators evaluate bottom-up, so decorators placed above @implement run after it and their metadata was invisible to the synthesized methods. Metadata copying and the ImplementInterceptor registration are now deferred with queueMicrotask, which fires after the whole decorator stack has been applied but before NestJS reads any handler metadata during app initialization. The documented ordering restriction is gone.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/hibernation
@orpc/json-schema
@orpc/nest
@orpc/next
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
✅ No new issues found. The fix is correct, well-verified, and the test coverage is strong.
Reviewed changes — Reviewed the full PR against main (5965f87). Router-contract @Implement now forwards method-level NestJS enhancers to the synthesized route handlers in either decorator order.
packages/nest/src/implement.ts— each synthesized handler inherits from the original router method viaObject.setPrototypeOf, so Nest's function-stored metadata (@UseGuards,@UsePipes,@UseFilters,@SetMetadata, interceptor/param metadata) resolves throughReflect.getMetadata(key, getHandler()). The method-metadata copy and theImplementInterceptorregistration are deferred withqueueMicrotask, making decorator placement around@Implementirrelevant and guaranteeing user interceptors always wrap outsideImplementInterceptor.packages/nest/src/implement.test.ts— the conflict-method-names test now boots a real Nest app; new guard and interceptor e2e tests exercise both decorator orders (plus a nested procedure), and runtime@Reqinjection /@SetMetadatavisibility viaReflectoronctx.getHandler()are asserted. All of these genuinely fail without the fix.apps/content/docs/integrations/nest.md— the ordering restriction warning is replaced with a note that decorators combine with@Implementin any order.
ℹ️ Ordering contract
The fix is sound today, but its correctness depends on the queueMicrotask-registered metadata copy and ImplementInterceptor running before Nest's route metadata scan. This holds for every realistic bootstrap (await NestFactory.create(...), await app.init(), .compile()), and all 74 tests pass — no action needed. It is a load-bearing ordering assumption that a future synchronous metadata read would silently break (guards would vanish again, reverting the security fix), so it is worth keeping in mind if this area is touched later.
openrouter/deepseek/deepseek-v4-flash-0731 (free via Pullfrog for OSS) | 𝕏
…order ImplementInterceptor is now registered at the decorated method level, so the original method's function object carries user interceptors and ImplementInterceptor in native decorator-evaluation order, and synthesized methods inherit the full ordered list through the prototype chain. The router branch no longer recurses through Implement itself: routing and status decorators are applied directly to synthesized methods, deferred until after the metadata copies so route metadata is always written last. Interceptors below @implement observe the encoded response; interceptors above it run inside and observe the raw implemented procedure - matching plain NestJS methods, and identical between single-procedure and router-contract implementations.
There was a problem hiding this comment.
✅ No new issues found. The interceptor-order rework is correct and well-tested.
Reviewed changes — Reviewed the delta since the prior review (8f8de0ce): the new commit reworks interceptor ordering around @Implement so it follows decorator order.
packages/nest/src/implement.ts—ImplementInterceptoris now registered once on the decorated method'sdescriptor.valueinstead of being registered per synthesized leaf. For router-contract methods this merges user interceptors andImplementInterceptorinto a single order-aware list that every synthesized method inherits through the prototype chain, fixing a latent bug where a leaf's own deferredImplementInterceptorregistration would shadow the user interceptors inherited from the router method.- Single-procedure
@Implementlikewise now registersImplementInterceptorsynchronously (previously deferred), so its position — and whether a user interceptor observes the rawProcedureor the encoded response — follows decorator placement, aligned with the router-contract branch. packages/nest/src/implement.test.ts— added interceptor-order e2e tests for both single-procedure and router-contract@Implementin both decorator orders, asserting what the user interceptor observes (rawProcedurevs encoded response). Extended the conflict-method-names test to boot a real app and assert@SetMetadatafrom both sides of@Implementis visible viaReflectoronctx.getHandler(). The ordering assertions genuinely discriminate the new behavior.apps/content/docs/integrations/nest.md— the info note now documents that execution order follows decorator order.
All 76 tests pass. The queueMicrotask-ordering contract flagged in the prior review (deferred metadata copy must flush before Nest's asynchronous route scan) is unchanged by this commit, and every realistic bootstrap satisfies it.
openrouter/deepseek/deepseek-v4-flash-0731 (free via Pullfrog for OSS) | 𝕏
@Implement routes

Method-level NestJS enhancers (
@UseGuards,@UsePipes,@UseFilters,@SetMetadata, ...) on a router-contract@Implementmethod never reached the synthesized route handlers — guards silently never ran, leaving those routes unprotected regardless of decorator order. Enhancer metadata now reaches every synthesized route, decorator placement around@Implementno longer matters for whether enhancers apply, and interceptor execution order follows decorator order exactly like on plain NestJS methods.Fixes
@SetMetadataresolve through NestJS'sReflect.getMetadatalookups and now run on every route, including nested routers.ImplementInterceptoris registered at the decorated method level, so the interceptor list carries user interceptors andImplementInterceptorin native decorator-evaluation order: interceptors below@Implementobserve the encoded response, interceptors above it run inside and observe the raw implemented procedure — identical between single-procedure and router-contract implementations.Implementitself: routing and status decorators are applied directly to synthesized methods, deferred (with the method-name-keyed metadata copies) until the whole decorator stack has run, so late-running decorators are picked up and route metadata is always written last.@Implementin any order and that execution order follows decorator order.Testing
AuthGuarde2e test: requests without the token are rejected (403) and with it succeed (200) on all synthesized routes including a nested procedure, in both decorator orders. Before the fix the guard never executed.@Implement, in both decorator orders, asserting what the user interceptor observes (encoded response vs raw procedure).@Req()injection works on synthesized methods, and@SetMetadatafrom both sides of@Implementis visible viaReflectoronctx.getHandler()at runtime.