--- 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