Dependency injection in Nest.js

Dependency injection in Nest.js

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:

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:

In this case, the SmtpService will be created and managed by the IoC container in Nest.js.

Registration in the module

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.
  • Testing: It is easy to mock dependency in tests:

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

The @Injectable() decorator allows Nest to register this class as a provider in the IoC container.

Step 2: Dependency Injection

ConstructorExampleController automatically receives an instance of ExampleService via DI.

Step 3: Registering in the module

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.

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.

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.

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.

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.