2

API with NestJS #148. Understanding the injection scopes

 1 month ago
source link: https://wanago.io/2024/04/01/api-nestjs-injection-scopes/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

API with NestJS #148. Understanding the injection scopes

JavaScript NestJS

April 1, 2024
This entry is part 148 of 148 in the API with NestJS

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,
  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,
  Post,
  Query,
  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: {
    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

Screenshot-from-2024-03-31-21-30-18.png

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<unknown>) {
    this.logger = new Logger(this.parentClass.constructor.name);

Thanks to using @Inject(INQUIRER), we can access the parent class that used our LoggerService. Because of that, we no longer need to manually provide the name of each class every time we want to use the logger.

articles.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../database/prisma.service';
import { ArticleNotFoundException } from './article-not-found.exception';
import { LoggerService } from '../logger/logger.service';
@Injectable()
export class ArticlesService {
  constructor(
    private readonly prismaService: PrismaService,
    private readonly loggerService: LoggerService
  async getById(id: number) {
    const article = await this.prismaService.article.findUnique({
      where: {
    if (!article) {
      this.loggerService.logger.warn('Tried to get an article that does not exist');
      throw new ArticleNotFoundException(id);
    return article;
  // ...

NestJS creates an instance of the loggerService specifically for the ArticlesService. The loggerService is aware that it was created specifically for the ArticlesService and creates a new instance of the Logger.

Summary

In this article, we’ve discussed injection scopes and their use. With the request scope, we can create services and controllers that are reinitialized for each request. This can come in handy when we want to react to request headers, including the JSON Web Token. However, to avoid performance issues, we need to avoid overusing request-scoped providers.

With transient-scoped providers, we can create a dedicated instance for each consumer. This can be useful when we want to configure our service differently for various consumers. To understand that, we created a logger service that is configured based on the provider that imports it.

The knowledge of various injection scopes can definitely come in handy when dealing with various more advanced use cases in NestJS.

Series Navigation<< API with NestJS #147. The data types to store money with PostgreSQL and Prisma

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK