Chapter 04 — Providers & Dependency Injection
Chapter 04 — Providers & Dependency Injection
Hey everyone! Welcome back to Namaste NestJS! 🙏
This is the most important chapter in the whole series. If controllers are the receptionist, providers are the chefs, engineers, and specialists who do the actual work. And Dependency Injection (DI) is the system that magically delivers those specialists to whoever needs them — without you ever typing new.
DI sounds intimidating. It isn't. By the end of this chapter you'll say "oh, that's it?" Let's dissolve the magic.
What we will cover:
- What a provider is (services and beyond)
- The problem DI solves — the "new" nightmare
- What Dependency Injection actually means
- How Nest wires it up (@Injectable + constructor)
- The DI container and provider tokens
- Provider scope (singleton by default)
- Custom providers (useValue, useClass, useFactory)
- Common mistakes
- Interview Questions
1. What is a Provider?
A provider is any class you can inject into another class. The most common kind is a service, but repositories, factories, and helpers are all providers too.
┌─────────────────────────────────────────────────────────────┐
│ WHAT IS A PROVIDER? │
├─────────────────────────────────────────────────────────────┤
│ │
│ Provider = a class marked @Injectable() that Nest can │
│ create once and hand ("inject") to anyone │
│ who asks for it. │
│ │
│ Services, repositories, helpers, factories... │
│ → if it can be injected, it's a provider. │
│ │
└─────────────────────────────────────────────────────────────┘
import { Injectable } from '@nestjs/common';
@Injectable() // 👈 THIS is what makes it a provider
export class CatsService {
private cats = [];
create(cat) { this.cats.push(cat); }
findAll() { return this.cats; }
}
In simple words: the @Injectable() decorator is a sticky note that says "Nest, you're allowed to manage and inject me."
2. The Problem DI Solves — The "new" Nightmare
Let's first feel the pain without DI, so you appreciate what it fixes.
THE PROBLEM — creating dependencies by hand:
==============================================
class CatsController {
private catsService;
constructor() {
// I need a CatsService... so I build it myself:
const db = new Database('localhost', 5432, user, pass); // 😩
const logger = new Logger(); // 😩
this.catsService = new CatsService(db, logger); // 😩
}
}
See the mess? The controller now has to know how to build a database, a logger, and a service. Problems pile up:
❌ Every class that needs CatsService rebuilds the whole chain ❌ Change CatsService's constructor → fix it in 20 places ❌ You get a NEW database connection every time (wasteful!) ❌ Testing is hell — you can't swap in a fake service
A simple analogy: imagine every time you wanted coffee, you first had to build a coffee machine, wire the electricity, and plumb the water. Insane, right? You just want someone to hand you a coffee. That "someone" is Dependency Injection.
3. What Dependency Injection Actually Means
Here's the whole idea in one line:
┌─────────────────────────────────────────────────────────────┐ │ │ │ Dependency Injection = Don't build your dependencies. │ │ ASK for them, and let Nest │ │ deliver ready-made instances. │ │ │ │ You "declare a need" in the constructor. │ │ Nest sees it and "injects" the right object. │ │ │ └─────────────────────────────────────────────────────────────┘
The same controller, with DI:
@Controller('cats')
export class CatsController {
// "I need a CatsService." — that's all you say.
constructor(private catsService: CatsService) {} // 👈 DI happens HERE
// ▲ ▲
// │ └── the TYPE tells Nest WHAT to inject
// └── `private` makes it a class property automatically
@Get()
findAll() {
return this.catsService.findAll(); // it's just... there. ready to use.
}
}
That's the power of DI. You never wrote new CatsService(...). You declared a need by type, and Nest fulfilled it.
4. How Nest Wires It Up — Step by Step
Three ingredients make injection work. Miss one and you get the famous "Nest can't resolve dependencies" error.
THE 3 INGREDIENTS: ────────────────── 1. Mark the class @Injectable() → CatsService 2. Register it in a module's providers[] → CatsModule 3. Ask for it by type in a constructor → CatsController
// 1. the provider
@Injectable()
export class CatsService { /* ... */ }
// 2. register it
@Module({
controllers: [CatsController],
providers: [CatsService], // 👈 "CatsService lives in this module"
})
export class CatsModule {}
// 3. ask for it
export class CatsController {
constructor(private catsService: CatsService) {} // 👈 delivered!
}
Now let's trace what Nest does at startup, by hand:
HAND TRACE: app boot → who gets what?
───────────────────────────────────────
1. Nest reads CatsModule
2. Sees providers: [CatsService]
→ creates ONE instance of CatsService, stores it in the container
3. Sees controllers: [CatsController]
→ wants to create it, but its constructor needs CatsService
4. Nest looks in the container → finds the CatsService instance
→ injects it into CatsController's constructor
▼
Both are wired, using ONE shared CatsService ✔
5. The DI Container & Provider Tokens
Where does Nest "store" these instances? In the DI container (also called the IoC container) — a big registry that maps a token to an instance.
┌─────────────────────────────────────────────────────────────┐
│ THE DI CONTAINER (a map) │
├─────────────────────────────────────────────────────────────┤
│ │
│ TOKEN → INSTANCE │
│ ───── ──────── │
│ CatsService → (the one CatsService object) │
│ Logger → (the one Logger object) │
│ 'CONFIG_OPTIONS' → { apiKey: '...' } │
│ │
│ When a constructor asks for `CatsService`, Nest looks │
│ up that TOKEN and hands back the stored instance. │
│ │
└─────────────────────────────────────────────────────────────┘
Q: What is a "token"?
A: A token is the key Nest uses to find a provider. When you inject by class type (like CatsService), the class itself is the token. For non-class things (a string, a config object), you use a custom string or symbol token — more on that in the custom-providers section.
Note: the reason type-based injection works at all is TypeScript emitting metadata about your constructor's parameter types. We open that hood fully in Deep Dive 01.
6. Provider Scope — Singleton by Default
By default, Nest creates exactly one instance of each provider and shares it everywhere. This is the singleton scope.
DEFAULT (singleton): one CatsService shared by ALL who inject it CatsController ─┐ DogsController ─┼──→ [ the SAME CatsService instance ] ReportService ─┘ → efficient, and state is shared. This is what you want 99% of the time.
Two other scopes exist for special cases:
| Scope | Instances | When to use |
|---|---|---|
| DEFAULT (singleton) | One, shared app-wide | Almost always ⭐ |
| REQUEST | A new one per incoming request | Per-request state (e.g. current user context) |
| TRANSIENT | A new one for each injector | When each consumer needs its own copy |
Pro tip: request-scoped providers are convenient but slower (Nest rebuilds them every request). Reach for them only when you truly need per-request isolation.
7. Custom Providers — When a Class Isn't Enough
Sometimes you want to inject something that isn't a simple class — a config object, a value, or a class chosen at runtime. Nest gives you three tools.
useValue → inject a ready-made value/object (great for config, mocks) useClass → inject THIS class whenever THAT token is requested (swappable) useFactory → inject the RESULT of a function (dynamic / needs other deps)
@Module({
providers: [
// 1. useValue — inject a plain object under a custom token
{ provide: 'CONFIG', useValue: { apiKey: 'abc123' } },
// 2. useClass — swap the implementation behind a token
{ provide: PaymentService, useClass: StripePaymentService },
// 3. useFactory — build it with a function (can depend on others)
{
provide: 'DB_CONNECTION',
useFactory: (config) => new Database(config.url),
inject: ['CONFIG'], // 👈 factory's own dependencies
},
],
})
export class AppModule {}
To inject a custom (non-class) token, use @Inject:
export class ReportService {
constructor(@Inject('CONFIG') private config) {} // 👈 inject by string token
}
Q: Why is useClass so powerful?
A: It lets you swap implementations without touching consumers. Inject PaymentService everywhere; behind the scenes, wire it to StripePaymentService in prod and a FakePaymentService in tests. Nothing else changes. That's the essence of loose coupling.
8. Little Traps Beginners Fall Into
┌─────────────────────────────────────────────────────────────┐ │ COMMON MISTAKES ❌ │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Forgetting to add the service to providers: []. │ │ → "Nest can't resolve dependencies of X" error. │ │ │ │ Forgetting @Injectable() on the service class. │ │ → Same resolution error. │ │ │ │ Using `new CatsService()` yourself. │ │ → You lose the shared singleton + DI benefits. │ │ │ │ Needing a service from ANOTHER module but not exporting │ │ it there. → You must `exports: [CatsService]` (Ch 05). │ │ │ └─────────────────────────────────────────────────────────────┘
Interview Questions — Quick Fire!
Q: What is a provider in NestJS?
"A provider is any class that can be injected as a dependency — most commonly a service. You mark it with @Injectable and register it in a module's providers array. Nest then manages its lifecycle and injects it wherever it's requested."
Q: Explain Dependency Injection in your own words.
"Dependency Injection is a pattern where a class doesn't create its own dependencies — it declares them, usually in the constructor, and an external system provides ready-made instances. In Nest, the framework's IoC container reads the constructor's types, finds or creates the matching providers, and injects them. This makes code loosely coupled, reusable, and easy to test."
Q: What are the three ingredients required for injection to work?
"First, the class must be decorated with @Injectable. Second, it must be registered in a module's providers array. Third, it must be requested by type in a constructor. If any of these is missing, Nest throws a 'cannot resolve dependencies' error."
Q: What is the default scope of a provider?
"Singleton — Nest creates one instance and shares it across the whole application. There are also request scope, which creates a new instance per incoming request, and transient scope, which creates a new instance for each consumer. Singleton is the default and best choice for almost all cases."
Q: What are custom providers and when would you use them?
"Custom providers let you control what gets injected for a token. useValue injects a fixed value like a config object, useClass swaps in a specific implementation behind a token, and useFactory injects the result of a function that can itself depend on other providers. They're useful for configuration, swapping implementations between environments, and dynamic setup."
Key Points to Remember
| Concept | Key Takeaway |
|---|---|
| Provider | An @Injectable class Nest can create and inject. |
| DI | Declare needs in the constructor; Nest delivers instances. |
| 3 ingredients | @Injectable + register in providers[] + ask by type. |
| DI container | Maps a token → a stored instance. |
| Default scope | Singleton — one shared instance app-wide. |
| Custom providers | useValue / useClass / useFactory for non-trivial cases. |
What's Next?
We keep mentioning that providers and controllers live inside a module, and that a service must be exported to be shared. In Chapter 05 we master Modules — how to split your app into feature boxes, share providers between them, and build the dependency graph that Nest walks at startup. This completes your Season 1 foundation.
Keep coding, keep shipping! See you in the next one!
Post a Comment