Providers: Dependency Injection in NestJS

Understanding providers (services, repositories, factories, etc.), injecting dependencies, and the scope of providers.


Dependency Injection in NestJS

Understanding Dependency Injection

Dependency Injection (DI) is a software design pattern where an object receives other objects (that it depends on) from an external source rather than creating them itself. This promotes loose coupling, making code more maintainable, testable, and reusable. In simpler terms, instead of a class being responsible for creating its dependencies, those dependencies are "injected" into it.

NestJS, built on top of Node.js and TypeScript, leverages DI as a fundamental architectural concept. The NestJS injector is powered by `reflect-metadata`, which allows NestJS to automatically determine the dependencies of your classes based on their type annotations.

Why Use Dependency Injection?

  • Loose Coupling: Components are less dependent on each other, making changes easier without affecting other parts of the application.
  • Testability: Dependencies can be easily mocked or stubbed during testing, allowing for isolated unit tests.
  • Reusability: Components can be reused in different contexts with different dependencies.
  • Maintainability: Easier to understand and maintain the codebase due to clear separation of concerns.
  • Extensibility: Easier to extend the application with new features without modifying existing code.

Dependency Injection in NestJS Providers

In NestJS, providers are the fundamental building blocks of an application. They can be services, repositories, factories, helpers, and more. DI is primarily used within providers to inject dependencies into other providers.

Constructor Injection

Constructor injection is the most common and recommended way to inject dependencies in NestJS. You declare dependencies as constructor parameters, and NestJS automatically resolves and injects them.

 import { Injectable } from '@nestjs/common';
import { UsersService } from './users.service';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
} 

In this example:

  • `@Injectable()` decorator marks the `AuthService` as a provider.
  • The `constructor` takes `UsersService` as a parameter.
  • NestJS automatically creates an instance of `UsersService` and injects it into `AuthService` when `AuthService` is instantiated.

Property Injection

Property injection allows you to inject dependencies directly into class properties. While functional, it's generally not recommended as the primary approach because it makes dependencies less explicit and can complicate testing. Use constructor injection whenever possible. It is particularly useful where circular dependencies exist and constructor injection cannot work.

 import { Injectable, Inject } from '@nestjs/common';
import { UsersService } from './users.service';

@Injectable()
export class AuthService {
  @Inject(UsersService)
  private usersService: UsersService;

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
} 

In this example:

  • `@Inject(UsersService)` decorator is used to specify that the `usersService` property should be injected with an instance of `UsersService`.
Important: You typically need to configure the compiler option strictPropertyInitialization to false or use definite assignment assertion modifiers (e.g. `private usersService!: UsersService;`) when using property injection in TypeScript to avoid compiler errors.

Example Usage

This example illustrates how to use constructor injection within a controller:

 import { Controller, Get, Param } from '@nestjs/common';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Get('validate/:username/:password')
  async validate(
    @Param('username') username: string,
    @Param('password') password: string,
  ): Promise<any> {
    return this.authService.validateUser(username, password);
  }
} 

The AuthController injects AuthService via constructor injection, allowing it to access the validateUser method.