Understanding the injection scopes
When a NestJS application starts, it creates instances of various classes, such as controllers and services. By default, NestJS treats those classes as…
When a NestJS application starts, it creates instances of various classes, such as controllers and services. By default, NestJS treats those classes as singletons, where a particular class has only one instance. NestJS then shares the single instance of each provider across the entire application’s lifetime, creating a singleton provider scope. If you want to know more about the Singleton pattern, check out JavaScript design patterns #1. Singleton and the Module The singleton scope fits most use cases. Thanks to creating just one instance of each provider and sharing it with all consumers, NestJS can cache them and increase performance. However, in some cases, we might want to change the default behavior. The request scope To change the default provider scope, we must provide the scope property to the @Injectable() decorator. logged-in-user.service.ts import { Injectable, Scope } from '@nestjs/common'; @Injectable({ scope: Scope.REQUEST }) export class LoggedInUserService { // ... }Thanks to adding scope: Scope.REQUEST, our LoggedInUserService is initialized every time a user makes an HTTP request handled by a controller that uses this service. Using the request object The request object contains information about the HTTP request made to our API. Since our service is request-scoped, we can access the request object. logged-in-user.service.ts import { Inject, Injectable, Scope } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Request } from 'express'; @Injectable({ scope: Scope.REQUEST }) export class LoggedInUserService { constructor(@Inject(REQUEST) private request: Request) {} }It’s very common to implement authentication using JSON Web Tokens using the Passport library. With this approach, the information about the logged-in user is attached to the request object. If you want to know more about authentication with NestJS, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies To access the information about the user, we should create an interface that extends the Request interface imported from the Express library. request-with-user.interface.ts import { Request } from 'express'; import { Prisma } from '@prisma/client'; export interface RequestWithUser extends Request { user: Prisma.UserGetPayload<{ include: { address: true } }>; } Using the UserGetPayload type from Prisma we can tell TypeScript that user property includes the address fetched through a relationship. We’ve used this interface in other parts of our application before. articles.controller.ts import { Body, Controller, Post, Req, UseGuards, } from '@nestjs/common'; import { ArticlesService } from './articles.service'; import { CreateArticleDto } from './dto/create-article.dto'; import { JwtAuthenticationGuard } from '../authentication/jwt-authentication.guard'; import { RequestWithUser } from '../authentication/request-with-user.interface'; @Controller('articles') export default class ArticlesController { constructor(private readonly articlesService: ArticlesService) {} @Post() @UseGuards(JwtAuthenticationGuard) create(@Body() article: CreateArticleDto, @Req() request: RequestWithUser) { return this.articlesService.create(article, request.user.id); } // ... }In the above case, the user property is always defined thanks to using the JwtAuthenticationGuard. If we want our LoggedInUserService to handle a situation when the user is not logged in, we can modify the RequestWithUser and make the user property optional. One way to do that would be to use the type-fest library. logged-in-user.service.ts import { Inject, Injectable, Scope } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { SetOptional } from 'type-fest'; import { RequestWithUser } from '../authentication/request-with-user.interface'; @Injectable({ scope: Scope.REQUEST }) export class LoggedInUserService { constructor( @Inject(REQUEST) private readonly request: SetOptional<RequestWithUser, 'user'>, ) {} getAddress() { return this.request.user?.address; } }We can now use our service in a controller, for example. articles.controller.ts import { Body, Controller, Get, Post, Query, Req, UseGuards, } from '@nestjs/common'; import { ArticlesService } from './articles.service'; import { CreateArticleDto } from './dto/create-article.dto'; import { JwtAuthenticationGuard } from '../authentication/jwt-authentication.guard'; import { RequestWithUser } from '../authentication/request-with-user.interface'; import { ArticlesSearchParamsDto } from './dto/articles-search-params.dto'; import { LoggedInUserService } from '../users/logged-in-user.service'; @Controller('articles') export default class ArticlesController { constructor( private readonly articlesService: ArticlesService, private readonly loggedInUserService: LoggedInUserService, ) {} @Get() getAll(@Query() searchParams: ArticlesSearchParamsDto) { console.log(this.loggedInUserService.getAddress()); return this.articlesService.search(searchParams); } @Post() @UseGuards(JwtAuthenticationGuard) create(@Body() article: CreateArticleDto, @Req() request: RequestWithUser) { console.log(this.loggedInUserService.getAddress()); return this.articlesService.create(article, request.user.id); } // ... }We need to remember, though, that the request.user property will not be defined if we don’t use the JwtAuthenticationGuard, which requires the user to be logged in. The scope hierarchy A significant downside to having a request-scoped provider is that a controller that depends on a request-scoped provider is also request-scoped. Therefore, the ArticlesController is request-scoped because it uses the LoggedInUserService, which is request-scoped. Using request-scoped providers will affect our application’s performance. Even though NestJS relies on cache under the hood, it still has to create an instance of each request-scoped provider. Therefore, we should use request-scoped providers sparingly if performance is one of our priorities. The transient scope When implementing logging, it’s a good practice to create a separate instance of the Logger class for each of our services. This way, we can provide the context to the Logger constructor. articles.service.ts import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../database/prisma.service'; import { ArticleNotFoundException } from './article-not-found.exception'; @Injectable() export class ArticlesService { logger = new Logger(ArticlesService.name) constructor(private readonly prismaService: PrismaService) {} async getById(id: number) { const article = await this.prismaService.article.findUnique({ where: { id, }, }); if (!article) { this.logger.warn('Tried to get an article that does not exist'); throw new ArticleNotFoundException(id); } return article; } // ... }Thanks to this approach, we see that a particular log comes from the ArticlesService. If you want to know more about logging in NestJS, check out API with NestJS #113. Logging with Prisma or API with NestJS #50. Introduction to logging with the built-in logger and TypeORM With the default singleton scope, a single provider instance is shared across the entire application. However, with the transient scope, each provider receives a dedicated instance. We can use this to create a smart logger service. logger.service.ts import { Inject, Injectable, Logger, Scope } from '@nestjs/common'; import { INQUIRER } from '@nestjs/core'; import { Class } from 'type-fest'; @Injectable({ scope: Scope.TRANSIENT }) export class LoggerService { logger: Logger; constructor(@Inject(INQUIRER) private parentClass: Class