134 lines
3.9 KiB
Markdown
134 lines
3.9 KiB
Markdown
---
|
||
title: "Understanding Providers and Dependency Injection in NestJS"
|
||
date: 2026-09-28T18:50:00+05:45
|
||
draft: false
|
||
tags: ["nestjs", "backend"]
|
||
description: "Concept of dependency injection in NestJS"
|
||
---
|
||
|
||
When I first started working with NestJS, registering providers (and controllers) inside a module felt like nothing more than a chore.
|
||
|
||
It was one of those things I did because *“that’s just how Nest is designed.”*
|
||
|
||
Even after I learned that modules exist to build an application graph, which it uses internally to resolve relationships and dependencies between modules and providers, the idea still felt vague and abstract. I understood **what** was happening, but not really **why** it worked the way it did.
|
||
|
||
Recently, I decided to take a deeper dive into how providers are registered and how Nest actually injects them. It turned out to be far more interesting than I expected.
|
||
|
||
Here’s what I learned.
|
||
|
||
## First, let’s understand what Dependency Injection is.
|
||
|
||
Wikipedia defines Dependency Injection(DI) as:
|
||
|
||
> **Dependency Injection** is a programming technique in which an object or function receives other objects or functions that it requires, as opposed to creating them internally.
|
||
|
||
Let’s look at a simple example.
|
||
|
||
Imagine a restaurant application. When a user’s order is ready for pickup, a notification needs to be sent.
|
||
|
||
```tsx
|
||
class OrderService {
|
||
// code operating over orders
|
||
|
||
const notificationService = new NotificationService()
|
||
notificationService.notify(userId, message)
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
Here, `OrderService` handles order-related logic, and `NotificationService` handles notifications.
|
||
|
||
However, because `NotificationService` is instantiated directly inside `OrderService` using `new`, this **is not** dependency injection.
|
||
|
||
Now compare that with this:
|
||
|
||
```tsx
|
||
class OrderService {
|
||
constructor(
|
||
private readonly notificationService: NotificationService,
|
||
) {}
|
||
|
||
// code operating over orders
|
||
notificationService.notify(userId, message)
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
This is dependency injection.
|
||
|
||
`OrderService` no longer cares about **how** `NotificationService` is created. It simply declares that it needs one, and something else provides it. That instance can also be cached and reused across every class that needs it, if configuration is same.
|
||
|
||
## Why Dependency Injection?
|
||
|
||
Dependency Injection helps by:
|
||
|
||
- Reducing coupling between classes
|
||
- Making testing easier (you can inject mock dependencies)
|
||
- Encouraging modular, maintainable code
|
||
|
||
## How Dependency Injection Works in NestJS
|
||
|
||
In NestJS, the basic flow looks something like this:
|
||
|
||
1. Define a provider using `@Injectable()`
|
||
2. Register that provider with Nest
|
||
3. Ask Nest to inject it using constructor-based injection
|
||
|
||
Registration usually happens in a module:
|
||
|
||
```tsx
|
||
@Module({
|
||
providers: [DemoService],
|
||
controllers: [DemoController],
|
||
})
|
||
export class DemoModule {}
|
||
```
|
||
|
||
At this point, Nest becomes responsible for creating and managing instances of `DemoService`.
|
||
|
||
## Nest’s IoC Container (The Missing Piece)
|
||
|
||
NestJS uses an **IoC (Inversion of Control) container** to manage dependencies.
|
||
|
||
You can think of this container as a **key-value** **registry**:
|
||
|
||
- **Keys** are called *tokens*
|
||
- **Values** describe how to create or retrieve a dependency
|
||
|
||
When we write:
|
||
|
||
```tsx
|
||
providers: [DemoService]
|
||
```
|
||
|
||
Nest internally expands it into a standard provider definition:
|
||
|
||
```tsx
|
||
providers: [
|
||
{
|
||
provide: DemoService,
|
||
useClass: DemoService,
|
||
},
|
||
]
|
||
```
|
||
|
||
In this case:
|
||
|
||
- The **token** is `DemoService` (the class itself)
|
||
- `useClass` tells Nest which class to instantiate when that token is requested.
|
||
|
||
So when Nest encounters:
|
||
|
||
```tsx
|
||
constructor(private readonly demoService: DemoService) {}
|
||
```
|
||
|
||
1. Nest looks up the `DemoService` token in the IoC container
|
||
2. It finds the matching provider record
|
||
3. It resolves the provider (`useClass` in this case)
|
||
4. The instance is created (or reused, since providers are singletons by default)
|
||
5. The resolved instance is injected into the class
|
||
|