What is Dependency Injection (DI)?
Dependency Injection (DI) is a software design pattern that allows separation of object dependencies and their initialization. In the context of Nest.js, DI enables easier dependency management, reducing class interdependence and increasing application modularity and testability. DI is a software pattern that is not only related to nest.js or node.js but is also used in other programming languages, read how dependency injection is used in general in the article What is “Dependency Injection”?.
DI is easiest to explain through an example, imagine you have a class that sends emails to users. That class can directly instantiate the object that sends the email (the SMTP service), or you can use Dependency Injection (DI) to leave that task to an external entity (the IoC container).
Example without Dependency Injection (Bad Practice)
In this example, an instance of the Smpt service is created in the constructor:
|
1 2 3 4 5 6 7 8 9 10 11 |
class EmailService { private smtpService: SmtpService; constructor() { this.smtpService = new SmtpService(); // Creating dependencies within a class } sendEmail(to: string, message: string): void { this.smtpService.send(to, message); } } |
SmtpService is a dependency (eng. dependency) for EmailService, and creating its instance in the constructor provides the so-called tight coupling, because the EmailService class depends on the concrete implementation of SmtpService. Creating our own instance in the constructor leads us to several problems:
- Difficult to change the service because any change in the implementation of the SMTP service would, for example, adding new dependencies to the SMTP service would require changes to every class where that class is instantiated,
- Difficult testing because it is not possible to easily mock SmtpService in tests.
Example with Dependency Injection (Good practice)
We will now use DI by moving the responsibility of creating dependencies from the EmailService class to an external IoC container:
|
1 2 3 4 5 6 7 8 9 10 |
import { Injectable } from '@nestjs/common'; @Injectable() export class EmailService { constructor(private readonly smtpService: SmtpService) {} // Zavisnost se injektuje sendEmail(to: string, message: string): void { this.smtpService.send(to, message); } } |
In this case, the SmtpService will be created and managed by the IoC container in Nest.js.
Registration in the module
|
1 2 3 4 5 6 |
import { Module } from '@nestjs/common'; @Module({ providers: [SmtpService, EmailService], // IoC kontejner registruje provajdere }) export class AppModule {} |
Key differences
- Responsibility for creating dependencies:
- Without DI: The class itself creates a dependency instance.
- With DI: the IoC container creates an instance and provides it to the class.
- Flexibility: Changing the SmtpService service itself or even replacing it with another service is simple — now it is enough to replace the provider in the IoC container, without changing the code in the EmailService.
12345678910@Module({providers: [{provide: SmtpService,useClass: AlternativeEmailService, // Zamena implementacije},EmailService,],})export class AppModule {} - Testing: It is easy to mock dependency in tests:
12const mockSmtpService = { send: jest.fn() };const emailService = new EmailService(mockSmtpService as SmtpService);
DI in practice with Nest.js
Instead of having classes create their dependencies themselves, DI passes responsibility for creating dependencies to some external entity — usually a dependency injection container. DI is a practical implementation of a broader concept known as Inversion of Control (IoC). IoC reverses the usual program flow — instead of the application controlling how dependencies are created and used, the IoC container takes over that task. In Nest.js, the IoC container automatically manages dependencies and their lifecycle based on defined rules, and the basic components used by DI in Nest.js are:
- Providers: Injectable classes or objects.
- Modules: Organize the application and define which providers are available.
- Decorators: Mark classes, methods, or parameters to indicate their roles in the DI system.
Step 1: Define the provider
|
1 2 3 4 5 6 7 8 |
import { Injectable } from '@nestjs/common'; @Injectable() export class ExampleService { getHello(): string { return 'Hello, Dependency Injection!'; } } |
The @Injectable() decorator allows Nest to register this class as a provider in the IoC container.
Step 2: Dependency Injection
|
1 2 3 4 5 6 7 8 9 10 11 12 |
import { Controller, Get } from '@nestjs/common'; import { ExampleService } from './example.service'; @Controller('example') export class ExampleController { constructor(private readonly exampleService: ExampleService) {} @Get() getHello(): string { return this.exampleService.getHello(); } } |
ConstructorExampleController automatically receives an instance of ExampleService via DI.
Step 3: Registering in the module
|
1 2 3 4 5 6 7 8 9 |
import { Module } from '@nestjs/common'; import { ExampleService } from './example.service'; import { ExampleController } from './example.controller'; @Module({ controllers: [ExampleController], providers: [ExampleService], }) export class ExampleModule {} |
Interface in DI for greater flexibility
Using interfaces in Nest.js allows applications greater flexibility and adaptability. Instead of using a specific implementation directly, you can define an interface that describes the behavior (contract) expected from the provider. This allows for easy implementation changes without requiring changes to controllers or other dependent components.
|
1 2 3 |
export interface ExampleInterface { getHello(): string; } |
Registering an interface in a module
An interface and its implementation are registered as providers within a module. The provide key is used to associate the implementation with the interface.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
import { Module } from '@nestjs/common'; import { ExampleImplementation } from './example.implementation'; @Module({ providers: [ { provide: 'ExampleInterface', useClass: ExampleImplementation, }, ], }) export class ExampleModule {} |
This approach allows changing the implementation by simply replacing the class in useClass, without requiring any changes to the controllers or the service.
Interface implementation
In this example, ExampleInterface defines the getHello method, which must be implemented by any class that registers as a provider.
|
1 2 3 4 5 6 7 8 |
import { Injectable } from '@nestjs/common'; @Injectable() export class ExampleImplementation implements ExampleInterface { getHello(): string { return 'Hello from implementation!'; } } |
The class ExampleImplementation implements ExampleInterface. This implementation will be used in controllers when the interface
registers in the module.
Using the interface in the controller
In a controller, an interface can be injected using the @Inject() decorator. The ‘ExampleInterface’ key is used to connect to the appropriate provider.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { Controller, Get, Inject } from '@nestjs/common'; import { ExampleInterface } from './example.interface'; @Controller('example') export class ExampleController { constructor( @Inject('ExampleInterface') private readonly exampleService: ExampleInterface, ) {} @Get() getHello(): string { return this.exampleService.getHello(); } } |
This allows the controller to use the functionality defined by the interface, while the implementation is controlled by the IoC container.
Advantages of this approach
- Simple implementation replacement: You can change the class that implements the interface without changing the controller.
- Increased testability: Instead of a real implementation, during testing you can register a mock or stub version of the interface.
- Modularity: Allows code to be separated into independent modules with minimal interdependencies.
- Adaptability: Different implementations are easily integrated, such as variants of functionality for different configurations or environments.

