Node.js Microservice with NestJS: Part 3 - NestJS Dependency Injection
An Introduction to Dependency Injection in NestJS
- Node.js Microservice with NestJS: Part 1 - Service Overview - An overview of the microservice we will be building
- Node.js Microservice with NestJS: Part 2 - Using the NestJS CLI - Provisioning our microservice scaffolding using the NestJS CLI.
What is Dependency Injection and Why do I need it?
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.: This decorator marks a class as a provider that can be injected as a dependency.
@Injectable(): Allows injecting custom providers or tokens.
@Inject(): Decorates a class to act as a controller in the application, usually having dependencies injected into it (e.g., services).
@Controller()
Creating a Service
A 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;
}
}
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, providers, exports 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 {}
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:
- Instantiation of modules and providers: It resolves dependencies and provides instances of required services.
- Lifecycle hooks: Providers and controllers can implement lifecycle hooks like
onModuleInitoronApplicationShutdownto 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
Get the Code
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
Post a Comment