NestJS is a progressive Node.js framework designed for building efficient, reliable, and scalable server-side applications. One of its core principles is Dependency Injection (DI), which provides a structured and modular approach to managing dependencies in your application. In this blog, we’ll dive deep into NestJS's DI system, explaining how decorators like @Injectable() make it all possible, and the role of metadata in enhancing DI.
What is Dependency Injection?
Dependency Injection is a design pattern that allows you to inject dependencies into a class, rather than hard-coding them inside the class. This promotes better testability, maintainability, and reusability of code. In NestJS, DI is at the heart of its architecture, and it’s seamlessly integrated using decorators and metadata.
Key Concepts
- Providers: These are classes or values that can be injected into other classes.
- Injectors: NestJS uses injectors to resolve and provide the correct dependencies.
- Decorators: Special markers used to add metadata to classes, methods, or properties, instructing NestJS how to handle them.
- Metadata: Behind the scenes, metadata provides the framework with the necessary information about the dependencies.
The @Injectable() Decorator
At the core of NestJS's DI system is the @Injectable() decorator. This decorator marks a class as a provider, which means that the class can be injected into another class via the constructor.
Example: A Simple Service
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
getUsers() {
return ['John Doe', 'Jane Doe', 'Mary Jane'];
}
}
In this example, the UserService is decorated with @Injectable(), which tells NestJS that this class is a provider and can be injected into other components, such as controllers or other services.
Injecting Dependencies in a Controller
To see the power of DI, let’s inject this UserService into a controller:
import { Controller, Get } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
findAll() {
return this.userService.getUsers();
}
}
Here, the UserController depends on UserService to get a list of users. Notice how we didn’t create an instance of UserService manually — it was injected into the controller via the constructor. This decouples the controller from the creation of the service, making the code more modular and testable.
How Does Dependency Injection Work in NestJS?
Under the hood, NestJS uses a powerful inversion of control (IoC) container that manages the lifecycle of all providers. When you decorate a class with @Injectable(), NestJS registers it in the IoC container. When a dependent class (like a controller) requires that provider, the IoC container supplies it, resolving any dependencies automatically.
Custom Providers with the @Inject() Decorator
In some cases, you may need to inject a custom value or object. NestJS allows you to do this with the @Inject() decorator:
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class ConfigService {
constructor(@Inject('DATABASE_URL') private databaseUrl: string) {}
getDatabaseUrl() {
return this.databaseUrl;
}
}
Here, we’re injecting a string (DATABASE_URL) into ConfigService. This string could be defined in a module's providers array.
@Module({
providers: [
{
provide: 'DATABASE_URL',
useValue: 'mongodb://localhost:27017/mydb',
},
ConfigService,
],
})
export class AppModule {}
Provider Scopes in NestJS
By default, providers in NestJS are singleton, meaning a single instance of the provider is shared across the entire application. However, you can change the scope of a provider using the scope option:
- Singleton: One instance for the whole app (default).
- Request-scoped: A new instance for each request.
- Transient: A new instance for each injection.
Here’s how you can define a request-scoped provider:
@Injectable({ scope: Scope.REQUEST })
export class ScopedService {
// This will be created per request
}
Decorators and Metadata
NestJS makes heavy use of decorators, which are a TypeScript feature that allows adding behavior to classes, methods, and properties. Decorators provide metadata that NestJS uses to configure how the DI system works.
For example, when you use the @Controller(), @Injectable(), or @Get() decorators, NestJS stores metadata about these classes and methods. This metadata is later used by the IoC container to inject dependencies, handle requests, and perform other tasks.
You can manually retrieve or define metadata using TypeScript’s Reflect API:
import 'reflect-metadata';
class Example {
@Reflect.metadata('role', 'admin')
getRole() {}
}
const role = Reflect.getMetadata('role', Example.prototype, 'getRole');
console.log(role); // Outputs: 'admin'
While you don’t need to use Reflect manually in most cases, understanding that decorators are built on metadata can help you appreciate how flexible NestJS’s DI system is.
Real-World Example: Injecting a Service with Dependencies
Let’s extend our earlier example with a DatabaseService that the UserService depends on.
import { Injectable } from '@nestjs/common';
@Injectable()
export class DatabaseService {
query(sql: string) {
return `Executing query: ${sql}`;
}
}
@Injectable()
export class UserService {
constructor(private readonly dbService: DatabaseService) {}
getUsers
() {
return this.dbService.query('SELECT * FROM users');
}
}
Here, UserService depends on DatabaseService, which is injected through the constructor. When UserService.getUsers() is called, it delegates to DatabaseService.query(). Again, thanks to DI, we don’t have to worry about creating or managing DatabaseService instances — NestJS handles that for us.
Summary
NestJS’s Dependency Injection system is one of its most powerful features, providing a clean, modular, and testable way to manage dependencies in your application. Through the use of decorators like @Injectable(), @Inject(), and the IoC container, NestJS makes it easy to write scalable and maintainable code.
Key Takeaways:
- @Injectable() is used to mark a class as a provider, making it injectable.
- Providers are injected through constructors, promoting loose coupling.
- NestJS uses metadata, defined through decorators, to manage DI under the hood.
- Custom providers can be created using @Inject(), and provider scopes allow control over provider lifetimes.
With this understanding of DI in NestJS, you’re well on your way to writing expressive, maintainable, and scalable applications.
