When we dive into the world of modern web development, especially with frameworks like NestJS, we encounter a lot of concepts that are both powerful and a bit complex to grasp at first. One of these concepts is Dependency Injection (DI), a design pattern used extensively in building scalable and easily maintainable applications. In NestJS, this pattern is beautifully simplified through the use of decorators like @Injectable
. Let's explore how this little piece of code makes our development life easier.
The Basics: What is @Injectable
?
In simple terms, when you annotate a class with @Injectable()
, you are essentially storing metadata using the reflect-metadata library, then when you add the class to the providers array you're telling NestJS: "Hey, please manage this class for me!" What does that mean? It means that NestJS will take care of creating an instance of this class and manage its entire lifecycle, based on the scope you define—singleton, request, or transient. But there’s more to it.
Consider a simple service in a NestJS app, like CarService
:
export class CarService {
getMake(): string {
return 'Tesla';
}
}
Without following NestJS's conventions, you might end up manually creating an instance of CarService
inside your controllers or other services. However, this approach is not just cumbersome but also contrary to the principles of good software architecture that NestJS advocates.
The Right Way: Leveraging DI
Instead of manually handling the object lifecycle, you should lean on NestJS's powerful DI system. By marking CarService
as @Injectable
and registering it in a module, NestJS handles its instantiation and ensures that it's ready to be injected wherever it's needed:
@Injectable()
export class CarService {
getMake(): string {
return 'Tesla';
}
}
@Module({
imports: [],
controllers: [AppController],
providers: [AppService, CarService /* register as a provider */],
})
export class AppModule {}
Now, CarService
can be easily injected into any controller or service without the need to manually create an instance:
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly carService: CarService,
) {}
@Get()
getHello(): string {
console.log(this.carService.getMake());
return this.appService.getHello();
}
}
What If You Forget @Injectable
?
Interestingly, even if you don’t explicitly use @Injectable()
in your classes, NestJS's DI mechanism might still handle the injection correctly if the class doesn’t depend on other providers. However, this works until it doesn't—especially when your classes start having dependencies of their own.
For instance, if CarService
requires LoggerService
, you must decorate CarService
with @Injectable()
to ensure that LoggerService
is properly injected:
@Injectable()
export class LoggerService {
log(message: string): void {
console.log(message);
}
}
export class CarService {
constructor(private readonly loggerService: LoggerService) {}
getMake(): string {
console.log(this.loggerService); // 💣💥
return 'Tesla';
}
}
If CarService is not properly decorated with @Injectable
, this.loggerService
would be undefined
.
Conclusion: Embrace the Simplicity
NestJS's DI system is robust and designed to simplify your development process. By using @Injectable()
, you ensure that your services are properly managed and that dependencies are handled correctly. This not only makes your code cleaner and more modular but also aligns with best practices that allow for greater scalability and easier maintenance.
So, next time you’re setting up services in your NestJS application, remember the importance of @Injectable()
. It’s a small annotation that carries a lot of weight, making your coding journey much smoother and enjoyable. Happy coding!
Top comments (2)
No, when you add
@Injectable()
in front of a class, you are scanning that class and storing metadata using thereflect-metadata
library. When you add providers to theproviders
array, only then do you ask NestJS "Hey, please manage this class for me!"Thanks for the clarification, will update.