Injection scopes

For people coming from different programming language backgrounds, it might be unexpected to learn that in Nest, almost everything is shared across incoming requests. We have a connection pool to the database, singleton services with global state, etc. Remember that Node.js doesn’t follow the request/response Multi-Threaded Stateless Model in which every request is processed by a separate thread. Hence, using singleton instances is fully safe for our applications.

However, there are edge-cases when request-based lifetime may be the desired behavior, for instance per-request caching in GraphQL applications, request tracking, and multi-tenancy. Injection scopes provide a mechanism to obtain the desired provider lifetime behavior.

Provider scope

A provider can have any of the following scopes:

SINGLETONA single instance of the provider is shared across the entire application. The instance lifetime is tied directly to the application lifecycle. Once the application has bootstrapped, all singleton providers have been instantiated. Singleton scope is used by default.
REQUESTA new instance of the provider is created exclusively for each incoming request. The instance is garbage-collected after the request has completed processing.
TRANSIENTTransient providers are not shared across consumers. Each consumer that injects a transient provider will receive a new, dedicated instance.

Hint Using singleton scope is recommended for most use cases. Sharing providers across consumers and across requests means that an instance can be cached and its initialization occurs only once, during application startup.

Usage

Specify injection scope by passing the scope property to the @Injectable() decorator options object:

  1. import { Injectable, Scope } from '@nestjs/common';
  2. @Injectable({ scope: Scope.REQUEST })
  3. export class CatsService {}

Similarly, for custom providers, set the scope property in the long-hand form for a provider registration:

  1. {
  2. provide: 'CACHE_MANAGER',
  3. useClass: CacheManager,
  4. scope: Scope.TRANSIENT,
  5. }

Hint Import the Scope enum from @nestjs/common

Notice Gateways should not use request-scoped providers because they must act as singletons. Each gateway encapsulates a real socket and cannot be instantiated multiple times.

Singleton scope is used by default, and need not be declared. If you do want to declare a provider as singleton scoped, use the Scope.DEFAULT value for the scope property.

Controller scope

Controllers can also have scope, which applies to all request method handlers declared in that controller. Like provider scope, the scope of a controller declares its lifetime. For a request-scoped controller, a new instance is created for each inbound request, and garbage-collected when the request has completed processing.

Declare controller scope with the scope property of the ControllerOptions object:

  1. @Controller({
  2. path: 'cats',
  3. scope: Scope.REQUEST,
  4. })
  5. export class CatsController {}

Scope hierarchy

Scope bubbles up the injection chain. A controller that depends on a request-scoped provider will, itself, be request-scoped.

Imagine the following dependency graph: CatsController <- CatsService <- CatsRepository. If CatsService is request-scoped (and the others are default singletons), the CatsController will become request-scoped as it is dependent on the injected service. The CatsRepository, which is not dependent, would remain singleton-scoped.

Learn the right way!

  • Injection scopes - 图1 60+ chapters
  • Injection scopes - 图2 4+ hours of videos
  • Injection scopes - 图3 Official certificate
  • Injection scopes - 图4 Deep-dive sessions

Explore official courses

Request provider

In an HTTP server-based application (e.g., using @nestjs/platform-express or @nestjs/platform-fastify), you may want to access a reference to the original request object when using request-scoped providers. You can do this by injecting the REQUEST object.

  1. import { Injectable, Scope, Inject } from '@nestjs/common';
  2. import { REQUEST } from '@nestjs/core';
  3. import { Request } from 'express';
  4. @Injectable({ scope: Scope.REQUEST })
  5. export class CatsService {
  6. constructor(@Inject(REQUEST) private request: Request) {}
  7. }

Because of underlying platform/protocol differences, you access the inbound request slightly differently for Microservice or GraphQL applications. In GraphQL applications, you inject CONTEXT instead of REQUEST:

  1. import { Injectable, Scope, Inject } from '@nestjs/common';
  2. import { CONTEXT } from '@nestjs/graphql';
  3. @Injectable({ scope: Scope.REQUEST })
  4. export class CatsService {
  5. constructor(@Inject(CONTEXT) private context) {}
  6. }

You then configure your context value (in the GraphQLModule) to contain request as its property.

Performance

Using request-scoped providers will have an impact on application performance. While Nest tries to cache as much metadata as possible, it will still have to create an instance of your class on each request. Hence, it will slow down your average response time and overall benchmarking result. Unless a provider must be request-scoped, it is strongly recommended that you use the default singleton scope.