Providers: Dependency Injection in NestJS

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


Dependency Injection in NestJS

Introduction

Dependency Injection (DI) is a design pattern that promotes loose coupling between classes by providing dependencies to a class instead of hardcoding them within the class itself. NestJS, built on TypeScript, provides a robust and elegant DI system based on the Angular DI framework.

Core Concepts of Dependency Injection in NestJS

What is a Dependency?

In the context of DI, a dependency is an object that another object needs to function correctly. It's a service or component that a class relies on. For example, a UserService might depend on a UserRepository to persist user data.

The Injector

The NestJS injector is responsible for creating and managing dependencies. It maintains a container of registered providers (services, components, etc.) and resolves dependencies when they are needed. NestJS handles the injector automatically.

Providers

Providers are the objects that are managed by the NestJS injector. They can be services, repositories, factories, or any other value that can be injected into a class. You register providers in modules using the providers array.

Injection

Injection is the process of providing a dependency to a class. NestJS uses constructor injection, meaning that dependencies are injected into the class constructor. You specify the dependencies you need using constructor parameters.

How Dependency Injection Works in NestJS

  1. Define a Provider: Create a class that represents the dependency you want to inject. Mark it as injectable using the @Injectable() decorator.
  2. Register the Provider: Add the provider to the providers array in a module using the @Module() decorator.
  3. Inject the Dependency: In the class that needs the dependency, declare it as a constructor parameter. NestJS will automatically resolve and inject the dependency when the class is instantiated.

Example

Let's illustrate with a simple example. Assume we have a CatsService that depends on a CatsRepository to interact with a database.

// cats.service.ts
import { Injectable } from '@nestjs/common';
import { CatsRepository } from './cats.repository';

@Injectable()
export class CatsService {
  constructor(private readonly catsRepository: CatsRepository) {}

  async findAll(): Promise<Cat[]> {
    return this.catsRepository.findAll();
  }

  async create(cat: Cat): Promise<Cat> {
    return this.catsRepository.create(cat);
  }
}

// cats.repository.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsRepository {
  async findAll(): Promise<Cat[]> {
    // Logic to fetch cats from database
    return [{ id: 1, name: 'Whiskers' }]; // Dummy data for example
  }

  async create(cat: Cat): Promise<Cat> {
    // Logic to save cat to database
    return cat; // Dummy data for example
  }
}

// cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { CatsRepository } from './cats.repository';

@Module({
  controllers: [CatsController],
  providers: [CatsService, CatsRepository],
})
export class CatsModule {}


// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
import { CreateCatDto } from './dto/create-cat.dto';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto): Promise<Cat> {
    return this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

Explanation:

  • @Injectable() decorator marks CatsService and CatsRepository as providers.
  • The CatsModule registers both CatsService and CatsRepository as providers in the providers array.
  • The CatsService constructor takes CatsRepository as a parameter. NestJS automatically injects an instance of CatsRepository when CatsService is created.
  • The CatsController constructor takes CatsService as a parameter. NestJS automatically injects an instance of CatsService when CatsController is created.

Benefits of Dependency Injection

  • Loose Coupling: Classes are less dependent on specific implementations of their dependencies, making them easier to test and maintain.
  • Modularity: You can easily swap out dependencies without modifying the classes that depend on them.
  • Testability: You can easily mock or stub dependencies during testing, allowing you to isolate and test individual classes.
  • Reusability: Dependencies can be reused in multiple classes.
  • Maintainability: Code becomes more organized and easier to understand.

Conclusion

Dependency Injection is a fundamental concept in NestJS that promotes well-structured, maintainable, and testable applications. By understanding and utilizing DI, you can build robust and scalable NestJS applications with ease.