Mastering NestJS Custom Providers: Reusable, Scoped Services & Injection Tokens
Learn how to build reusable, scoped services in NestJS using custom providers, dynamic modules, and injection tokens. This guide walks through a concrete example, trade‑offs, and verification steps to help you decide the right approach for your application.
08 Jul 2026, 16:39 UTC

Problem: Tight Coupling and Hard‑to‑Test Services
When a NestJS application grows, services often start to depend on concrete implementations rather than abstractions. This tight coupling makes unit tests brittle, feature toggles hard to enable, and memory usage unpredictable. The core of the problem is that the framework’s default provider behavior—singleton, class‑based registration—doesn’t expose enough flexibility for advanced use cases.
Thesis: Leverage Custom Providers, Scopes, and Tokens
NestJS’s provider system is intentionally flexible. By defining providers with @Injectable(), choosing a scope, and using custom injection tokens, you can:
- Encapsulate logic into reusable modules.
- Control instance lifecycle to match request or transient needs.
- Decouple consumers from concrete implementations.
- Resolve circular dependencies safely.
1. Creating a Reusable Custom Provider
Start with a simple service that formats dates. Instead of hard‑coding the format, expose it via a token.
// date.formatter.ts
import { Injectable } from "@nestjs/common";
export const DATE_FORMATTER = Symbol("DATE_FORMATTER");
@Injectable()
export class DateFormatter {
format(date: Date, fmt: string = "YYYY-MM-DD"): string {
// Simple implementation; real code would use a library like dayjs
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
}
}
Register it in a module:
// date.module.ts
import { Module } from "@nestjs/common";
import { DateFormatter, DATE_FORMATTER } from "./date.formatter";
@Module({
providers: [
DateFormatter,
{ provide: DATE_FORMATTER, useExisting: DateFormatter },
],
exports: [DATE_FORMATTER],
})
export class DateModule {}
Now any consumer can inject DATE_FORMATTER without knowing the concrete class.
2. Controlling Lifecycle with Scopes
Singleton providers are the default, but they can become memory hogs or cause stale state in multi‑tenant scenarios. NestJS supports three scopes:
- Singleton – one instance per application (default).
- Request – new instance per HTTP request.
- Transient – new instance every injection.
Example: a TenantService that holds tenant‑specific configuration. It should be request‑scoped to avoid leaking data between users.
// tenant.service.ts
import { Injectable, Scope } from "@nestjs/common";
@Injectable({ scope: Scope.REQUEST })
export class TenantService {
private tenantId: string;
constructor() {
// In a real app, tenant ID would come from request context
this.tenantId = Math.random().toString(36).substring(2, 8);
}
getId() { return this.tenantId; }
}
Verification: make two separate HTTP requests to a controller that injects TenantService and compare the returned IDs. Different IDs confirm separate instances.
3. Dynamic Modules for Runtime Flexibility
Dynamic modules let you expose providers based on runtime conditions—ideal for feature toggles or multi‑tenant setups.
// feature.module.ts
import { DynamicModule, Module } from "@nestjs/common";
import { FeatureService } from "./feature.service";
@Module({})
export class FeatureModule {
static forFeature(enabled: boolean): DynamicModule {
const providers = enabled
? [FeatureService]
: [{ provide: FeatureService, useValue: null }];
return {
module: FeatureModule,
providers,
exports: [FeatureService],
};
}
}
Now you can conditionally register FeatureService based on an environment variable.
4. Resolving Circular Dependencies Safely
Two services that depend on each other can break the container. NestJS offers forwardRef() to break the cycle, but overuse signals a design flaw.
// auth.service.ts
import { Injectable, forwardRef, Inject } from "@nestjs/common";
import { UserService } from "./user.service";
@Injectable()
export class AuthService {
constructor(
@Inject(forwardRef(() => UserService))
private readonly userService: UserService,
) {}
// ...
}
// user.service.ts
import { Injectable, forwardRef, Inject } from "@nestjs/common";
import { AuthService } from "./auth.service";
@Injectable()
export class UserService {
constructor(
@Inject(forwardRef(() => AuthService))
private readonly authService: AuthService,
) {}
// ...
}
Run the application and confirm no runtime errors. If the cycle persists, consider refactoring into a shared interface or a mediator service.
Trade‑Offs and Limitations
- Memory Footprint – Request‑scoped providers consume more memory per request. Use them only when per‑request state is required.
- Startup Time – Large singleton providers can slow initial boot. Lazy‑load modules or use
useFactoryto defer heavy initialization. - Complexity – Custom tokens and dynamic modules add indirection. Keep documentation tight and avoid over‑engineering.
- Circular Dependencies –
forwardRef()should be a last resort. Excessive use indicates a need for architectural review.
Actionable Checklist
- Identify services that could benefit from isolation: logging, configuration, tenant context.
- Choose an appropriate scope: singleton for stateless, request for per‑request data, transient for lightweight stateless helpers.
- Expose services via injection tokens to decouple consumers.
- Use dynamic modules to toggle features or isolate tenant modules.
- Run integration tests that make multiple HTTP requests to verify scope behavior.
- Audit for circular dependencies; refactor if
forwardRef()is used more than once.
Conclusion
Custom providers, scopes, and injection tokens are powerful tools in NestJS that transform a monolithic service layer into a modular, testable, and memory‑efficient architecture. By following the patterns and verification steps above, you can make informed decisions about when to use singletons, request scopes, or dynamic modules, and avoid the pitfalls of circular dependencies and bloated memory usage.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.