Proxy Providers
Since
v3.0
This feature was inspired by how REQUEST-scoped providers ("beans") work in the Spring framework for Java/Kotlin.
Using this technique, NestJS does not need to re-create a whole DI-subtree on each request (which has certain implications which disallows the use of REQUEST-scoped providers in certain situations).
Rather, it injects a SINGLETON Proxy instance, which delegates access and calls to the actual instance, which is created for each request when the CLS context is set up, but this does not affect the scope of any provider that the Proxy is injected into.
There are two kinds of Proxy providers - Class and Factory.
Please note that there are some caveats to using this technique.
Class Proxy Providers
These providers look like your regular class providers, with the exception that is the @InjectableProxy() decorator to make them easily distinguishable.
@InjectableProxy()
export class User {
id: number;
role: string;
}
To register the proxy provider, use the ClsModule.forFeature() registration,
which exposes it an injectable provider in the parent module.
ClsModule.forFeature(User);
It can be then injected as usual using the class reference.
However, what will be actually injected is not the instance of the class, but rather the Proxy which redirects all access to an unique instance stored in the CLS context.
Populate in an enhancer
A Class provider defined in this way will be empty upon creation, so we must assign context values to it somewhere. One place to do it is an interceptor
@Injectable()
export class UserInterceptor implements NestInterceptor {
// we can inject the proxy here
constructor(private readonly user: User) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// and assign or change values as it was a normal object
this.user.id = request.user.id;
this.user.role = 'admin';
return next.handle();
}
}
Self-populating Proxy Provider
It is also possible to inject other providers into the Proxy Provider to avoid having to do this in a separate component.
For the convenience, the CLS_REQ and CLS_RES (if enabled) and CLS_CTX (when an enhancer is used) are also made into Proxy Providers and are exported from the ClsModule.
To prevent silent failures, they're marked as strict.
@InjectableProxy()
export class UserWithRole {
id: number;
role: string;
constructor(
@Inject(CLS_REQ) request: Request,
roleService: RoleService,
) {
this.id = request.user.id;
this.role = roleService.getForId(request.user.id);
}
}
If you need to inject a provider from an external module, use the ClsModule.forFeatureAsync() registration to import the containing module.
ClsModule.forFeatureAsync({
// make RoleService available to the Proxy provider
imports: [RoleModule],
useClass: UserWithRole,
});
Using @Inject(CLS_REQ), you can entirely replace @Inject(REQUEST) in REQUEST-SCOPED providers to turn them into CLS-enabled singletons without changing the implementation.
Also @INJECT(CLS_CTX) can be used to replace @Inject(CONTEXT).
Factory Proxy Providers
Like your normal factory providers, Proxy factory providers look familiar.
They can be only registered using the ClsModule.forFeatureAsync() method.
Here's an example of a hypothetical factory provider that dynamically resolves to a specific tenant database connection:
ClsModule.forFeatureAsync({
provide: TENANT_CONNECTION,
imports: [DatabaseConnectionModule],
inject: [CLS_REQ, DatabaseConnectionService],
useFactory: async (req: Request, dbService: DatabaseConnectionService) => {
const tenantId = req.params['tenantId'];
const connection = await dbService.getTenantConnection(tenantId);
return connection;
},
global: true, // make the TENANT_CONNECTION available for injection globally
});
Again, the factory will be called on each request and the result will be stored in the CLS context. The TENANT_CONNECTION provider, however, will still be a singleton and will not affect the scope of whatever it is injected into.
In the service, it can be injected using the provide token as usual:
@Injectable()
class DogsService {
constructor(
@Inject(TENANT_CONNECTION)
private readonly connection: TenantConnection,
) {}
getAll() {
return this.connection.dogs.getAll();
}
}
Delayed resolution of Proxy Providers
By default, proxy providers are resolved as soon as the setup function in an enhancer (middleware/guard/interceptor) finishes. For some use cases, it might be required that the resolution is delayed until some later point in the request lifecycle once more information is present in the CLS .
To achieve that, set resolveProxyProviders to false in the enhancer options and call (and await) ClsService#proxy.resolve() manually at any time.
ClsModule.forRoot({
middleware: {
resolveProxyProviders: false,
},
});
//... later
await this.cls.proxy.resolve();
If you want to use proxy providers that depend on CLS_CTX, but still want to perform some setup in middleware, you have to disable the resolution of Proxy Providers in the middleware and delay it until the interceptor.
ClsModule.forRoot({
middleware: {
generateId: true,
setup: (cls) => {
/* some setup */
},
resolveProxyProviders: false,
},
interceptor: {
generateId: false,
saveCtx: true,
resolveProxyProviders: true,
},
});
Outside web request
This might also be necessary outside the context of web request.
With cls.run()
If you set up the context with cls.run() to wrap any subsequent code thar relies on Proxy Providers, you must call ClsService#proxy.resolve() before accessing them, otherwise access to any property of the injected Proxy Provider will return undefined, that is because an unresolved Proxy Provider falls back to an empty object.
@Injectable()
export class CronController {
constructor(
private readonly someService: SomeService,
private readonly cls: ClsService,
);
@Cron('45 * * * * *')
async handleCron() {
await this.cls.run(async () => {
// prepare the context
this.cls.set('some-key', 'some-value');
// trigger Proxy Provider resolution
await this.cls.proxy.resolve();
await this.someService.doTheThing();
});
}
}
With @UseCls()
Since the @UseCls() decorator wraps the function body with cls.run() automatically, you can use the setup function to prepare the context.
The Proxy Providers will be resolved after the setup phase.
@Injectable()
export class CronController {
constructor(private readonly someService: SomeService);
@Cron('45 * * * * *')
@UseCls({
setup: (cls) => {
cls.set('some-key', 'some-value');
},
})
async handleCron() {
await this.someService.doTheThing();
}
}
Selective resolution of Proxy Providers
You can also selectively resolve a subset of Proxy Providers, by passing a list of their injection tokens to ClsService#proxy.resolve(tokens). This is useful if the providers need to be resolved in a specific order or when some part of the application does not need all of them.
// resolves ProviderA and ProviderB only
await this.cls.proxy.resolve([ProviderA, ProviderB]);
// ... later
// resolves the rest of the providers that have not been resolved yet
await this.cls.proxy.resolve();
Strict Proxy Providers
sincev4.4.0
By default, accessing an unresolved Proxy Provider behaves as if it was an empty object. In order to prevent silent failures, you can set the strict option to true in the proxy provider registration. In this case, any attempt to access a property or a method on an unresolved Proxy Provider will throw a ProxyProviderNotResolvedException.
For Class Proxy Providers, you can use the according option on the @InjectableProxy() decorator.
@InjectableProxy({
strict: true,
})
export class User {
id: number;
role: string;
}
In case of Factory Proxy Providers, use the option on the ClsModule.forFeatureAsync() registration.
ClsModule.forFeatureAsync({
provide: TENANT_CONNECTION,
import: [DatabaseConnectionModule],
inject: [CLS_REQ],
useFactory: async (req: Request) => {
// ... some implementation
},
strict: true,
});
Caveats
Proxy Providers require full application bootstrap
The Proxy Provider resolver is initialized in ClsRootModule.onModuleInit(). Any code that runs before app.init() (or app.listen()) resolves may encounter an uninitialized resolver and will not be able to use Proxy Providers.
It goes without saying that any access to any property on a Proxy provider in the constructor will always evaluate to undefined (or throw in strict mode).
This is a common pitfall with background job processors such as BullMQ workers, which start consuming messages in onModuleInit. If the worker's onModuleInit runs before ClsRootModule.onModuleInit() (which depends on module initialization order), Proxy Providers will not yet be available.
To avoid this, ensure the application is fully bootstrapped before your worker begins consuming. Either start consuming in onApplicationBootstrap (which is guaranteed to run after all onModuleInit hooks) or delay consumption until after app.listen() / app.init() resolves:
@Injectable()
export class WorkerService implements OnApplicationBootstrap {
constructor(private readonly worker: Worker) {}
// Use onApplicationBootstrap instead of onModuleInit
// to ensure Proxy Providers are available.
onApplicationBootstrap() {
this.worker.run();
}
}
Do not mix REQUEST-scoped providers with Proxy Providers
Never inject a real NestJS Scope.REQUEST (or durable: true) provider as a dependency of a Proxy Provider or any ClsModule plugin.
Proxy Providers are singletons from NestJS's DI perspective. When NestJS detects that a singleton depends on a REQUEST-scoped provider, it changes the scope of the singleton to REQUEST as well. This means the Proxy wrapper itself is re-created on every request, which defeats the purpose of Proxy Providers and can cause cross-request contamination (e.g. tenant connections leaking between requests).
// ❌ Wrong: TENANT_CONNECTION depends on a real Scope.REQUEST provider
{
provide: TENANT_CONNECTION,
scope: Scope.REQUEST,
durable: true,
inject: [REQUEST, TenantRegistry],
useFactory: (req: Request, registry: TenantRegistry) =>
registry.getConnection(req.headers['tenant-id']),
}
Convert it to a ClsModule.forFeatureAsync Proxy Provider using CLS_REQ or ClsService instead:
// ✅ Correct: factory is a singleton; request data comes from CLS context
ClsModule.forFeatureAsync({
provide: TENANT_CONNECTION,
inject: [CLS_REQ, TenantRegistry],
useFactory: (req: Request, registry: TenantRegistry) =>
registry.getConnection(req.headers['tenant-id']),
});
CLS_REQ is itself a Proxy Provider (a singleton that delegates to the per-request value stored in CLS), so the factory above remains a singleton from NestJS's point of view while still resolving the correct request on each access.
No primitive values
Proxy Factory providers cannot return a primitive value (string, number, boolean, null, or undefined). Doing so throws a ProxyProviderInvalidReturnTypeException at resolution time. This is because the provider itself is the Proxy and it only delegates access once a property or a method is called on it (or if it itself is called in case the factory returns a function).
function Proxies must be explicitly enabled
In order to support injecting proxies of functions, the underlying proxy target must be a function, too, in order to be able to implement the "apply" trap. However, this information cannot be extracted from the factory function itself, so if your factory returns a function, you must explicitly set the type property to function in the provider definition.
{
provide: SOME_FUNCTION,
useFactory: () => {
return () => {
// do something
};
},
type: 'function',
}
In versions prior to v4.0, calling typeof on an instance of a Proxy provider always returned function, regardless of the value it holds. This is no longer the case. Please see Issue #82
Support for injecting Proxy Providers into each other
Since
v5.3
Proxy Providers can be injected into other Proxy Providers and will be resolved in the correct order. They can be mixed and matched with regular providers, too.
Please note that there is no support for circular dependencies between Proxy Providers, even with forwardRef. There is a timeout of 10 seconds to resolve the Proxy Providers, after which an error is thrown. The most common cause of this is a circular dependency, because two providers wait before the other is resolved, causing a deadlock.
Before
v5.3
Apart from the built-in CLS_REQ, CLS_RES and CLS_CTX proxy providers, custom Proxy Providers cannot be reliably injected into other Proxy Providers, because there was no system in place to resolve them in the correct order (as far as Nest is concerned, all of them have already been bootstrapped, so it can't help us here), so it may happen, that during the proxy provider resolution phase, a Proxy Provider that is injected into another Proxy Provider is not yet resolved and falls back to an empty object.
If can't update to the latest version, but you need this feature, see the original feature request to address this shortcoming and refer to the manual Selective resolution of Proxy Providers technique. You can also leverage the strict mode to find out which Proxy Providers are not yet resolved.