Node.js Microservice with NestJS: Part 3 - NestJS Dependency Injection

An Introduction to Dependency Injection in NestJS

NestJS is a powerful framework for building efficient, reliable, and scalable server-side applications. One of the core concepts in NestJS is Dependency Injection (DI), which is built on the Inversion of Control (IoC) design pattern. In this third installment on building a microservice using NestJS, we'll explore what dependency injection is, how it's implemented in NestJS, and why it's crucial for building maintainable applications. We'll also provide practical examples  of how we use it in our tour-service application to illustrate these concepts and break down important topics like decorators, constructors, modules, application context, and services.


For more details about the service we are building and what we have done so far, check out the following posts:

What is Dependency Injection and Why do I need it?

Dependency Injection is a design pattern that deals with how components get hold of their dependencies. Instead of a class creating its dependencies, they are provided (or injected) by an external entity. This pattern is a specific form of the broader concept of Inversion of Control (IoC), which shifts the responsibility of control (e.g., object creation) from the object itself to an external source.

Benefits of Dependency Injection

  • Decoupling: Classes are decoupled from their dependencies, making the system more modular.
  • Testability: It becomes easier to swap real dependencies with mocks or stubs, enhancing unit testing.
  • Maintainability: Changes to dependencies don't require changes to the dependent class, reducing the impact of changes.

Inversion of Control (IoC)

IoC is a principle where the control of objects or portions of a program is transferred to a container or framework. This concept is pivotal in frameworks like NestJS, Spring, and Angular. By inverting control, the framework manages the creation and lifecycle of objects, leading to more manageable and testable code.


Dependency Injection in NestJS

NestJS implements DI in a declarative and decorator-based way, making it easy for developers to specify and inject dependencies. The use of TypeScript decorators such as @Injectable(), @Inject(), and @Controller() provides a clear and intuitive approach to defining dependencies.

Declarative and Decorator-Based

In NestJS, DI is declarative—meaning you don't manually manage dependency creation; you simply declare dependencies, and NestJS injects them at runtime. The framework achieves this through decorators, which are metadata annotations added to classes, methods, or parameters.

@Injectable()
: This decorator marks a class as a provider that can be injected as a dependency.

@Inject()
: Allows injecting custom providers or tokens.

@Controller()
: Decorates a class to act as a controller in the application, usually having dependencies injected into it (e.g., services).

Creating a Service

service in NestJS is a class that contains business logic and can be injected into controllers, other services, or any component that needs it.  Let's start by creating a simple service. In NestJS, services are usually classes annotated with the @Injectable() decorator.  This (along with the module configuration discussed below) makes them available for dependency injection.  The LandmarkService below denotes that it is injectable by using the @Injectable() decorator at the class level.

import { Injectable } from '@nestjs/common';
import {
DistanceUnit,
LocalLandmarkCriteria,
LocalLandmarkDetails,
} from '../common';

@Injectable()
export class LandmarkService {
public async getLocalLandmarks(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
criteria: LocalLandmarkCriteria,
): Promise<LocalLandmarkDetails[]> {
console.log(
'Request made to LandmarkService.getLocalLandmarks with criteria',
criteria,
);
const landmarks: LocalLandmarkDetails[] = [
{
landmark: {
id: 'lm_1',
title: 'The First Landmark',
url: 'https://en.wikipedia.org/wiki/Aaron_A._Sargent_House',
imageUrl: 'http://img1.url',
coordinates: {
latitude: 39.2663886,
longitude: -121.02655320000001,
},
provider: 'provider-1',
readableSummary: 'The First Landmark Summary',
},
distance: 55,
distanceUnit: DistanceUnit.FOOT,
},
];
console.log(
'LandmarkService.getLocalLandmarks returning landmarks',
landmarks,
);
return landmarks;
}
}


Here's an example of how the LandmarkService is then injected into LandmarkController using constructor-based injection:

import { Controller, Get, Query } from '@nestjs/common';
import {
DistanceUnit,
LocalLandmarkCriteria,
LocalLandmarkDetails,
} from '../common';
import { LandmarkService } from './landmark.service';

@Controller('landmark/v1')
export class LandmarkController {
private readonly landmarkService: LandmarkService;

// Injection via constructor
constructor(landmarkService: LandmarkService) {
this.landmarkService = landmarkService;
}

@Get('/landmark/local')
public async getLocalLandmarks(
@Query('latitude') latitude: number,
@Query('longitude') longitude: number,
@Query('distanceUnit') distanceUnit: DistanceUnit,
@Query('maxCount') maxCount: number,
): Promise<LocalLandmarkDetails[]> {
const criteria: LocalLandmarkCriteria = {
coordinates: {
latitude,
longitude,
},
maxCount,
distanceUnit,
};
console.log(
'Request made to LandmarkController.getLocalLandmarks with criteria',
criteria,
);
return this.landmarkService.getLocalLandmarks(criteria);
}
}

In this example, LandmarkService is injected into LandmarkController via the constructor, demonstrating how NestJS manages dependencies and keeps your codebase modular.


Constructor or Property-Based Injection

NestJS primarily uses constructor-based injection, meaning dependencies are injected into the constructor of a class as seen in the LandmarkController example above. This is the most common method because it ensures that all dependencies are initialized when the object is instantiated, making the class easier to test and more predictable.

However, property-based injection can also be used by applying the @Inject() decorator to a property.  If you want to learn more about differences between constructor-based and property-based injection and when each is appropriate, I go into them in detail in this post: Constructor-Based vs Property-Based Dependency Injection in NestJS - Which is Better?


Modules in NestJS

In NestJS, modules are a fundamental concept that helps organize code into logical units. Modules are containers for related controllers, services, and other providers, helping to keep the application structure modular and maintainable.

  • Every application has at least one root module (usually AppModule).
  • Modules are decorated using the @Module() decorator, which defines their controllers, providersexports and imports.

Here's an example of a basic module which we use to add the LandmarkService and LandmarkController into the application:

import { Module } from '@nestjs/common';
import { LandmarkService } from './landmark.service';
import { LandmarkController } from './landmark.controller';

@Module({
providers: [LandmarkService], // Services that can be injected into other components
controllers: [LandmarkController], // Controller(s) that belong to this module
exports: [LandmarkService], // Services available for injection outside this module
imports: [], // Where other modules this module depends on are included
})
export class LandmarkModule {}
providers:  These are the services that are made available for injection into other services within this module.  In the above example LandmarkService is made available for injection.

controllers:  This lists the Controllers that are to be registered with the NestJS application and exposed for receiving HTTP requests.  In the above example, we register the LandmarkController.

exports:  This lists any providers/services that we want to make available for injection outside of this module.  In the above example, we expose the LandmarkService.

imports:  This declares any other modules whose exposed providers/services that this module wants to inject.  In the above example we are not including any.  Note that any other module that wants to include the LandmarkService exposed by this module would need to include LandmarkModule in its imports array.


The App Module

The AppModule is the root module in a NestJS application and serves as the entry point for the app. It brings together other modules and dependencies into one place. Below is the AppModule for our app.

import { Module } from '@nestjs/common';
import { LandmarkModule } from './landmark/landmark.module';

@Module({
imports: [LandmarkModule], // Here we import other modules into the root module
controllers: [],
providers: [],
})
export class AppModule {}

The root module can import and configure other modules, services, and providers, creating a well-structured and scalable application.


Application Context

In NestJS, the application context is a central object responsible for managing the lifecycle of modules and their dependencies.  It is essentially a big map of all providers keyed to their names (which defaults to the class object for standard service classes decorated with @Injectable()).  The context is created when the application is bootstrapped. The application context handles:

  1. Instantiation of modules and providers: It resolves dependencies and provides instances of required services.
  2. Lifecycle hooks: Providers and controllers can implement lifecycle hooks like onModuleInit or onApplicationShutdown to handle initialization and shutdown tasks.

By relying on the application context, developers can focus on writing business logic while NestJS handles the intricate process of dependency management, module resolution, and provider instantiation.


Advanced Dependency Injection

The dependency injection covered in this article is pretty basic.  Perhaps I will cover more advanced dependency injection in a later post.  In the meantime, I highly recommend looking at the NestJS docs for some great examples of advanced dependency injection: https://docs.nestjs.com/providers


Get the Code

All of the code from this post can be found in the apps/tour-service directory of the nestjs-service-3-di git branch here: 

Final Thoughts

Dependency Injection is a core feature in NestJS that makes your application modular, testable, and scalable. NestJS uses IoC principles and decorators to make DI declarative and intuitive. By utilizing constructor-based injection, modules, application context, and services, developers can build loosely coupled applications that are easier to maintain and extend. The framework handles the heavy lifting of managing dependencies, allowing you to focus on writing business logic.

With these concepts, you are now ready to leverage DI to build clean, maintainable, and scalable applications in NestJS!

Comments

Popular posts from this blog

REST APIs with Controllers - Node.js Microservice with NestJS: Part 4

Constructor-Based vs Property-Based Dependency Injection in NestJS - Which is Better?