How Dependency Injection Works in NestJS

This article explains the core concepts of Dependency Injection (DI) in Node.js, focusing on how the NestJS framework implements this design pattern. You will learn about the Inversion of Control (IoC) container, the role of decorators like @Injectable(), and how dependency resolution simplifies application architecture and testing.


Understanding Dependency Injection and IoC

Dependency Injection is a design pattern used to achieve Inversion of Control (IoC) between classes and their dependencies. Instead of a class instantiating its own dependencies directly (using the new keyword), the responsibility of creating and delivering those dependencies is delegated to an external system—the IoC container.

In Node.js frameworks like NestJS, this pattern decouples your code, making components highly modular, reusable, and easy to test.


Core Components of NestJS Dependency Injection

NestJS relies on three main concepts to handle dependency injection: Providers, Consumers, and the IoC Container.

1. Providers

A provider is any class that can be injected as a dependency. In NestJS, you declare a class as a provider by decorating it with the @Injectable() decorator. This decorator attaches metadata that tells the NestJS IoC container that this class is available for injection.

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  findAll() {
    return [{ id: 1, name: 'John Doe' }];
  }
}

2. Consumers

Consumers are classes that require (depend on) a provider. Typically, these are Controllers or other Services. You inject a provider into a consumer through constructor injection. NestJS automatically resolves the dependency by reading the TypeScript design types.

import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  // NestJS automatically injects the UsersService instance here
  constructor(private readonly usersService: UsersService) {}

  @Get()
  getAllUsers() {
    return this.usersService.findAll();
  }
}

3. The IoC Container and Modules

For NestJS to wire these components together, you must register them within a Module. The Module acts as the configuration boundary where the IoC container looks to resolve dependencies.

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService], // Registered as a provider in the container
})
export class UsersModule {}

How NestJS Resolves Dependencies Under the Hood

When you start a NestJS application, the framework performs the following steps to resolve dependencies:

  1. Metadata Analysis: NestJS utilizes TypeScript’s metadata reflection (reflect-metadata) to inspect the constructor parameter types of your controllers and services.
  2. Dependency Graph Construction: The framework builds an internal directed acyclic graph of all registered modules, controllers, and providers to determine the correct instantiation order.
  3. Instantiation (Singleton Scope): By default, NestJS uses the Singleton pattern. The IoC container creates a single instance of a provider the first time it is needed and caches it. Any subsequent request for that provider receives the cached instance.
  4. Injection: The container instantiates the consumer (e.g., UsersController) and passes the pre-created instances of its dependencies (e.g., UsersService) directly into its constructor.

Benefits of Dependency Injection in Node.js