← Back to list

Mastering Providers, Tokens, and Scopes in NestJS DI

Understanding Provider Types, Injection Tokens, and Scopes in NestJS

Mansi Patel · 2025-04-17 04:06 · 16 claps · 9.1 min read
#nestjs #dependency-injection #nestjs-tutorial #nestjs-provider #nestjs-service
Open on Medium ↗

Mastering Providers, Tokens, and Scopes in NestJS DI

☕ Introduction: Behind the Counter

Dependency Injection (DI) is one of the core building blocks of NestJS. It allows different parts of your application to work together without being tightly coupled — kind of like how a well-run coffee shop operates. You don’t worry about how the coffee is made — you just place your order, and the system takes care of the rest.

Now imagine stepping behind the counter.

You’ll discover that the system behind each cup of coffee is powered by different kinds of ingredients and roles: a barista who crafts it (class), pre-packaged sugar (value), or a custom recipe based on the time of day (factory). To keep things organized, everything needs a label (token), and depending on how busy the shop is, drinks might be brewed once, per customer, or every time someone asks — these are your provider scopes.

In this post, we’re going deeper into NestJS Dependency Injection. You’ll learn:

  • The different types of providers NestJS supports and when to use each.
  • How to use tokens with @Inject() to reference your providers precisely.
  • The concept of scopes, and how they affect the lifecycle of your services.
  • And finally, we’ll peek into circular dependencies and testing DI, which often come into play as your app grows.

So grab your apron — it’s time to brew up some advanced DI skills.

☕ Types of Providers in NestJS = Types of Ingredients in a Coffee Shop

In NestJS, a provider is any class or value that can be injected as a dependency. But not all providers are created the same — some are simple values, some are classes, and some are built dynamically. Think of them as different types of ingredients or roles in a coffee shop.

Let’s break down the four main types:

1. Class Providers → The Barista

The most common provider type in NestJS is a class provider. You define a class, and NestJS takes care of instantiating it for you. It’s like having a barista in the shop — a trained individual who can make coffee anytime someone asks.

@Injectable()
export class CoffeeService {
  brew() {
    return 'Brewing coffee...';
  }
}

@Module({
  providers: [CoffeeService],
})

Whenever you inject CoffeeService, NestJS hands you a ready-to-work barista behind the counter.

2. Value Providers → Pre-packaged Sugar Packets

Sometimes you don’t need a class — you just need to provide a static value, like a config object or a constant. In the coffee world, these are pre-packaged sugar packets — simple, ready-to-use items that don’t require any preparation.

const coffeeConfig = {
  size: 'medium',
  strength: 'strong',
};

@Module({
  providers: [
    {
      provide: 'COFFEE_CONFIG',
      useValue: coffeeConfig,
    },
  ],
})

To use this value elsewhere, you’ll inject it with the @Inject() decorator and its token:

@Injectable()
export class CoffeeMaker {
  constructor(@Inject('COFFEE_CONFIG') private config) {}
}

3. Factory Providers → Custom Recipes Based on the Situation

Let’s say your coffee shop changes recipes based on time of day or weather. You need logic to decide what kind of coffee to serve. That’s where factory providers come in — they dynamically create a provider value using a function.

@Module({
  providers: [
    {
      provide: 'SPECIAL_DRINK',
      useFactory: () => {
        const hour = new Date().getHours();
        return hour < 12 ? 'Morning Latte' : 'Evening Espresso';
      },
    },
  ],
})

The drink depends on the time — it’s like asking the barista, “What’s good right now?” and letting them decide.

To use this provider elsewhere in your app, inject it using its token:

@Injectable()
export class DrinkService {
  constructor(@Inject('SPECIAL_DRINK') private readonly special: string) {}
  recommend() {
      return `Today's special: ${this.special}`;
    }
}

Now when you call recommend(), you’ll get either a “Morning Latte” or “Evening Espresso” based on the current time. Dynamic and delicious.

4. Existing Providers → Alias for the Same Ingredient

Sometimes, you want to refer to the same provider with different names — maybe for backward compatibility or clarity in different modules. This is like calling espresso “strong coffee” — same ingredient, just an alias.

@Injectable()
export class EspressoService {
  getShot() {
    return 'A shot of espresso!';
  }
}

@Module({
  providers: [
    EspressoService,
    {
      provide: 'STRONG_COFFEE',
      useExisting: EspressoService,
    },
  ],
})

Now whether you inject EspressoService or 'STRONG_COFFEE', you get the same shot.

Here’s how you can use the alias in a service:

@Injectable()
export class CoffeeClient {
  constructor(@Inject('STRONG_COFFEE') private readonly coffee: EspressoService) {}
  order() {
      return this.coffee.getShot(); // returns 'A shot of espresso!'
    }
}

This is helpful when you want to rename a service or expose it under a friendlier or more descriptive token without creating a new instance.

That’s your coffee shop’s ingredient list — from trained baristas to ready-made packets to custom recipes and aliases. In the next section, we’ll talk about how you label these ingredients using tokens so NestJS always knows exactly what you want to inject.

🏷️ Using Tokens with @Inject() = Labeling the Jars

In a busy coffee shop, not everything comes in a clearly labeled box. Imagine three similar-looking jars — one filled with sugar, one with salt, and one with powdered creamer. If you don’t label them correctly, things get… weird.

That’s exactly why tokens exist in NestJS — especially when you’re using Value Providers or Factory Providers. Since these don’t come from a class, NestJS can’t automatically figure out what to inject — so you must assign them a token, like a label on a jar.

🔖 Why Tokens Are Needed

NestJS can automatically recognize class-based providers by their type. But Value Providers (useValue) and Factory Providers (useFactory) don’t have a class — so NestJS needs you to explicitly give them a name (a token).

const config = { origin: 'Colombia', roast: 'dark' };

@Module({
  providers: [
    {
      provide: 'COFFEE_CONFIG', // <-- this is the token
      useValue: config,         // <-- this is a Value Provider
    },
  ],
})

This tells NestJS: “When someone asks for 'COFFEE_CONFIG', give them this object.”

📦 Injecting a Token-based Provider

To use a token-based provider (like the one above), you must use the @Inject() decorator with the correct token:

@Injectable()
export class BrewService {
  constructor(@Inject('COFFEE_CONFIG') private readonly config: any) {}
  getDetails() {
      return `Brewing ${this.config.roast} roast from ${this.config.origin}.`;
    }
}

If you don’t use @Inject(), NestJS won’t know what to inject — because it can't guess based on type.

🧠 Tip: Use Constants to Avoid Typos

Instead of repeating the string 'COFFEE_CONFIG' in multiple places, define it once as a constant to avoid typos and keep things maintainable:

export const COFFEE_CONFIG = 'COFFEE_CONFIG';
@Module({
  providers: [
    {
      provide: COFFEE_CONFIG,
      useValue: config,
    },
  ],
})

@Injectable()
export class BrewService {
  constructor(@Inject(COFFEE_CONFIG) private readonly config: any) {}
}

You can also use Symbol() or Nest’s InjectionToken utility for even better type safety — especially in large applications or shared libraries.

⚖ When You Need to Use Tokens

🌍 Provider Scopes = How Often You Brew the Coffee

In NestJS, when you register a provider (like a service), you also have control over how often that provider is instantiated. This concept is known as scope — and it determines whether a new instance is created per app, per request, or every time it’s injected.

Let’s imagine this in our coffe shop metaphor:

  • Do you have one coffee machine serving everyone?
  • A separate barista preparing drinks per customer?
  • Or a completely fresh drink every time someone places an item in their order?

That’s what NestJS scopes are all about: singleton, request, and transient.

🎛️ Singleton (Default) — One Coffee Machine for Everyone

This is the default scope. NestJS creates one instance of the provider when the app starts, and shares it with everyone.

@Injectable()
export class CoffeeMachineService {
  brewCount = 0;
  brew() {
      this.brewCount++;
      return `Brew #${this.brewCount}`;
    }
}

All services that inject CoffeeMachineService will get the same shared machine. It’s efficient and great for things like database connections or loggers.

👨‍🍳 Request Scope — One Barista Per Customer

With request scope, a new instance of the provider is created for each HTTP request. Every service inside that same request will share the same barista.

@Injectable({ scope: Scope.REQUEST })
export class BaristaService {
  constructor(@Inject(REQUEST) private readonly request: Request) {}
  serve() {
      return `Serving coffee to ${this.request.user?.name || 'Guest'}`;
    }
}

Use this when you want to access request-specific data like:

  • The current user
  • A tenant ID
  • A trace ID for logging

💡 Within one request, all injections share the same instance — one barista per customer.

⚠️ Heads-up: Request-scoped providers introduce performance overhead because they require more complex context handling behind the scenes. Use them only when needed.

🍶 Transient Scope — A Fresh Drink Every Time

With transient scope, NestJS creates a new instance every single time the provider is injected — even within the same request or service.

@Injectable({ scope: Scope.TRANSIENT })
export class PourOverService {
  pour() {
    return `Fresh pour-over at ${Date.now()}`;
  }
}

This is like making a fresh drink every time someone asks for one — even if they already ordered earlier.

Use it when:

  • You want unique values (timestamps, random IDs)
  • You need strict isolation per injection
  • You’re dealing with stateless utility classes

🔄 Comparing Scopes at a Glance

🔄 Circular Dependencies = When Two Baristas Wait on Each Other

Sometimes in a coffee shop (or a NestJS app), things get a little tangled.

Imagine two baristas:

  • Barista A can’t start the order without a nod from Barista B.
  • But Barista B is waiting on Barista A to begin.

Now nobody moves. That’s a circular dependency — and it happens in NestJS when two providers depend on each other.

🧵 The Problem

Let’s say OrderService needs PaymentService, and PaymentService also needs OrderService:

@Injectable()
export class OrderService {
  constructor(private paymentService: PaymentService) {}
}

@Injectable()
export class PaymentService {
  constructor(private orderService: OrderService) {}
}

When Nest tries to resolve these dependencies, it gets stuck — because it doesn’t know which one to instantiate first. This leads to a runtime error.

🧯 The Solution: forwardRef

To fix this, Nest provides the forwardRef() helper — a way of telling Nest:

“Hey, I know this looks circular, but trust me — resolve the other one later.”

Here’s how you break the cycle:

@Injectable()
export class OrderService {
  constructor(
    @Inject(forwardRef(() => PaymentService))
    private paymentService: PaymentService,
  ) {}
}

@Injectable()
export class PaymentService {
  constructor(
    @Inject(forwardRef(() => OrderService))
    private orderService: OrderService,
  ) {}
}

You wrap the class reference in forwardRef(() => ClassName). Nest will delay resolving it until all providers are registered — breaking the deadlock.

☕ Coffee Shop Analogy

It’s like this: You give each barista a sticky note saying “Wait for Barista B” or “Check back with Barista A later” — and the manager (NestJS) uses those notes to resolve things once everyone’s available.

🔍 When to Use It

Circular dependencies are not inherently bad, but they often signal a tight coupling between classes. Try to avoid them when possible by:

  • Refactoring shared logic into a third service
  • Using event-based communication
  • Extracting interfaces or abstract providers

✅ Testing DI in NestJS = Replacing the Real Barista with a Stand-in

When testing services in NestJS, you usually don’t want the real providers doing actual work — you just want to simulate behavior and focus on testing the service in isolation.

It’s like hiring a stand-in barista for training. The stand-in doesn’t need to know how to actually brew coffee — they just need to say “Order received!” when needed.

🧪 Using Test.createTestingModule()

NestJS makes it easy to create isolated test environments using the @nestjs/testing package. Here's how you can set up a simple testing module:

describe('CoffeeService', () => {
  let service: CoffeeService;
  beforeEach(async () => {
      const module = await Test.createTestingModule({
        providers: [CoffeeService],
      }).compile();
      service = module.get<CoffeeService>(CoffeeService);
    });
    it('should brew coffee', () => {
      expect(service.brew()).toBe('Brewing coffee...');
    });
});

This works great when CoffeeService doesn’t depend on anything. But what if it relies on other services?

🎭 Mocking Dependencies with useValue

Say CoffeeService depends on WaterService. You can mock WaterService like this:

const mockWaterService = {
  getWater: () => 'Mock water',
};

const module = await Test.createTestingModule({
  providers: [
    CoffeeService,
    {
      provide: WaterService,
      useValue: mockWaterService,
    },
  ],
}).compile();

This way, you avoid triggering real database calls, APIs, or heavy logic — and instead provide a simple mock implementation.

🧠 You Can Also Use:

  • useClass – for replacing the provider with a custom mock class
  • useFactory – if you need dynamic or async mocks
  • overrideProvider() – when modifying a provider in an existing module

☕ Coffee Shop Analogy

In test mode, you don’t want your real barista grinding beans and frothing milk — you just want someone to act like a barista and respond with “Order complete” so you can test the cashier’s behavior.

✅ When Testing DI:

  • Test the real service, but replace its dependencies
  • Keep tests fast, isolated, and focused
  • Use mocks to simulate edge cases and failures

🏁 Wrapping Up

Dependency Injection is more than just a pattern in NestJS — it’s the backbone of how services connect, communicate, and scale cleanly.

In this post, you stepped behind the counter of the coffee shop and explored:

  • The different types of providers (class, value, factory, existing)
  • How to use tokens and @Inject() to label your dependencies
  • What scopes mean for provider lifecycles (singleton, request, transient)
  • How to resolve circular dependencies with forwardRef
  • And how to test providers using mocks and stubs

Mastering these concepts gives you the power to design maintainable, testable, and flexible applications — with clean, decoupled architecture.

☕ Go forth and build — your DI skills are now brewed to perfection.


메타데이터
post_id
8de87aef3bf5
slug
mastering-providers-tokens-and-scopes-in-nestjs-di-8de87aef3bf5
url
https://medium.com/@mansipatel3104/mastering-providers-tokens-and-scopes-in-nestjs-di-8de87aef3bf5
canonical_url
https://medium.com/@mansipatel3104/mastering-providers-tokens-and-scopes-in-nestjs-di-8de87aef3bf5
author_url
https://medium.com/@mansipatel3104
status
ok
fetched_at
2026-09-19 03:49:38