Request Scope
nest-trpc-native supports NestJS request-scoped providers, giving each tRPC procedure invocation its own provider instance.
Creating a Request-Scoped Provider
import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
@Injectable({ scope: Scope.REQUEST })
export class RequestMetaService {
public readonly requestId: string;
constructor(@Inject(REQUEST) request: any) {
this.requestId = request.headers?.['x-request-id'] ?? crypto.randomUUID();
}
}
Using in a Router
@Router('users')
class UsersRouter {
constructor(private readonly meta: RequestMetaService) {}
@Query()
me() {
return { requestId: this.meta.requestId };
}
}
Each call to trpc.users.me.query() gets a fresh RequestMetaService instance with the current request's metadata.
Module Registration
@Module({
providers: [UsersRouter, RequestMetaService],
})
export class UsersModule {}
Performance Trade-offs
What Happens Under the Hood
When any provider in a module is request-scoped (Scope.REQUEST), NestJS creates a new DI sub-tree for every incoming request. This means:
- Every provider that depends on (or is depended upon by) the scoped provider is also re-instantiated.
- The Nest injector resolves the full dependency chain from scratch each time.
- Singleton services injected alongside request-scoped ones are not affected — but the router class itself becomes transient.
Cost Comparison
| Approach | Instance creation | DI resolution | Memory | Best for |
|---|---|---|---|---|
| Singleton (default) | Once at startup | Once | Minimal | Stateless services, shared caches |
@TrpcContext() | None (decorator extraction) | None | Zero overhead | Per-request data (user, requestId, headers) |
Scope.REQUEST | Every request | Full sub-tree | Higher | Per-request stateful services |
When to Use Each
Use @TrpcContext() when you only need to read request data:
@Router('users')
class UsersRouter {
@Query()
me(@TrpcContext('requestId') requestId: string) {
return { requestId };
}
}
Zero allocation cost — the decorator simply extracts a value from the context object that createContext already built.
Use Scope.REQUEST when you need a stateful service per request:
@Injectable({ scope: Scope.REQUEST })
export class AuditTrail {
private readonly entries: string[] = [];
record(action: string) {
this.entries.push(action);
}
getEntries() {
return [...this.entries];
}
}
This is the right choice when the service accumulates state across multiple method calls within the same request — something a decorator parameter cannot do.
Rules of Thumb
- Start with singletons +
@TrpcContext()— covers 90%+ of use cases with no performance cost. - Reach for
Scope.REQUESTonly when you need mutable per-request state (audit trails, request-scoped caches, multi-step transactions). - Keep request-scoped providers in leaf modules — avoid scoping providers in shared/core modules, as it forces the entire dependency chain to become request-scoped.
- Never scope a provider just to access the request object — use
createContext+@TrpcContext()instead.
context-request-scope demonstrates both patterns side by side: @TrpcContext('requestId') for simple extraction and RequestMetaService for stateful per-request data.