Skip to main content

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.

There are two kinds of Proxy providers - Class and Factory.

note

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 using the class name.

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 are also made into Proxy Providers and are exported from the ClsModule.

@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
import: [RoleModule],
useClass: UserWithRole,
});
tip

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.

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,
import: [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();
}
}

Caveats

No primitive values

Proxy Factory providers cannot return a primitive value. 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',
}
note

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

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 ClsService#resolveProxyProviders() manually at any time.

ClsModule.forRoot({
middleware: {
resolveProxyProviders: false,
},
});

Outside web request

This is also necessary outside the context of web request, otherwise all access to an injected Proxy Provider will return undefined.

With cls.run()

If you set up the context with cls.run() to wrap any subsequent code thar relies on Proxy Providers.

@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.resolveProxyProviders();
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();
}
}