NestJS Dependency Injection
Master NestJS dependency injection for building modular, testable Node.js applications with proper service architecture, provider patterns, and module organization.
Table of Contents
-
Provider Patterns
-
Module System
-
Injection Scopes
-
Advanced Patterns
-
Best Practices
-
Common Pitfalls
-
Resources
Provider Patterns
Class Providers (Standard Pattern)
import { Injectable } from '@nestjs/common';
@Injectable() export class UserService { private users: User[] = [];
findAll(): User[] { return this.users; }
findById(id: string): User | undefined { return this.users.find(user => user.id === id); }
create(user: User): User { this.users.push(user); return user; } }
// Module registration @Module({ providers: [UserService], exports: [UserService], }) export class UserModule {}
Value Providers
import { Module } from '@nestjs/common';
// Simple value provider const DATABASE_CONNECTION = { provide: 'DATABASE_CONNECTION', useValue: { host: 'localhost', port: 5432, database: 'mydb', }, };
// Configuration value provider const APP_CONFIG = { provide: 'APP_CONFIG', useValue: { apiUrl: process.env.API_URL, timeout: 5000, retries: 3, }, };
@Module({ providers: [DATABASE_CONNECTION, APP_CONFIG], exports: [DATABASE_CONNECTION, APP_CONFIG], }) export class ConfigModule {}
// Usage in service @Injectable() export class ApiService { constructor( @Inject('APP_CONFIG') private config: AppConfig, ) {}
async fetchData(): Promise<any> { const response = await fetch(this.config.apiUrl, { timeout: this.config.timeout, }); return response.json(); } }
Factory Providers
import { Injectable, Module } from '@nestjs/common';
// Simple factory provider const CONNECTION_FACTORY = { provide: 'DATABASE_CONNECTION', useFactory: () => { return createConnection({ type: 'postgres', host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT), database: process.env.DB_NAME, }); }, };
// Factory with dependencies const CACHE_MANAGER = { provide: 'CACHE_MANAGER', useFactory: (config: ConfigService) => { return createCacheManager({ store: config.get('CACHE_STORE'), ttl: config.get('CACHE_TTL'), max: config.get('CACHE_MAX_ITEMS'), }); }, inject: [ConfigService], };
@Module({ providers: [ ConfigService, CONNECTION_FACTORY, CACHE_MANAGER, ], exports: ['DATABASE_CONNECTION', 'CACHE_MANAGER'], }) export class DatabaseModule {}
Async Providers with useFactory
import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config';
const DATABASE_PROVIDER = { provide: 'DATABASE_CONNECTION', useFactory: async (config: ConfigService) => { const connection = await createConnection({ type: 'postgres', host: config.get('DB_HOST'), port: config.get('DB_PORT'), username: config.get('DB_USER'), password: config.get('DB_PASSWORD'), database: config.get('DB_NAME'), });
await connection.runMigrations();
return connection;
}, inject: [ConfigService], };
const REDIS_PROVIDER = { provide: 'REDIS_CLIENT', useFactory: async (config: ConfigService) => { const client = createClient({ url: config.get('REDIS_URL'), });
await client.connect();
client.on('error', (err) => {
console.error('Redis error:', err);
});
return client;
}, inject: [ConfigService], };
@Module({ providers: [DATABASE_PROVIDER, REDIS_PROVIDER], exports: ['DATABASE_CONNECTION', 'REDIS_CLIENT'], }) export class DataModule {}
Token-Based Injection with String Tokens
import { Inject, Injectable, Module } from '@nestjs/common';
// Define string tokens as constants export const LOGGER_TOKEN = 'LOGGER'; export const METRICS_TOKEN = 'METRICS'; export const API_CLIENT_TOKEN = 'API_CLIENT';
// Provider definitions const LOGGER_PROVIDER = { provide: LOGGER_TOKEN, useFactory: () => { return createLogger({ level: process.env.LOG_LEVEL || 'info', format: 'json', }); }, };
const METRICS_PROVIDER = { provide: METRICS_TOKEN, useValue: createMetricsClient(), };
@Module({ providers: [LOGGER_PROVIDER, METRICS_PROVIDER], exports: [LOGGER_TOKEN, METRICS_TOKEN], }) export class ObservabilityModule {}
// Usage in service @Injectable() export class UserService { constructor( @Inject(LOGGER_TOKEN) private logger: Logger, @Inject(METRICS_TOKEN) private metrics: MetricsClient, ) {}
async createUser(data: CreateUserDto): Promise<User> { this.logger.info('Creating user', { email: data.email }); this.metrics.increment('user.created');
const user = await this.repository.create(data);
return user;
} }
Token-Based Injection with Symbol Tokens
import { Inject, Injectable, Module } from '@nestjs/common';
// Define symbol tokens for better type safety export const DATABASE_CONNECTION = Symbol('DATABASE_CONNECTION'); export const CACHE_MANAGER = Symbol('CACHE_MANAGER'); export const EVENT_BUS = Symbol('EVENT_BUS');
// Provider with symbol token const DB_PROVIDER = { provide: DATABASE_CONNECTION, useFactory: async () => { return await createDatabaseConnection(); }, };
const CACHE_PROVIDER = { provide: CACHE_MANAGER, useClass: RedisCacheManager, };
@Module({ providers: [DB_PROVIDER, CACHE_PROVIDER], exports: [DATABASE_CONNECTION, CACHE_MANAGER], }) export class InfrastructureModule {}
// Usage with symbol tokens @Injectable() export class ProductService { constructor( @Inject(DATABASE_CONNECTION) private db: Connection, @Inject(CACHE_MANAGER) private cache: CacheManager, ) {}
async findById(id: string): Promise<Product> {
const cached = await this.cache.get(product:${id});
if (cached) return cached;
const product = await this.db
.getRepository(Product)
.findOne({ where: { id } });
await this.cache.set(`product:${id}`, product, 3600);
return product;
} }
Optional Dependencies with @Optional()
import { Injectable, Optional, Inject } from '@nestjs/common';
@Injectable() export class NotificationService { constructor( @Optional() @Inject('EMAIL_SERVICE') private emailService?: EmailService, @Optional() @Inject('SMS_SERVICE') private smsService?: SmsService, ) {}
async notify(user: User, message: string): Promise<void> { // Gracefully handle missing optional dependencies if (this.emailService) { await this.emailService.send(user.email, message); }
if (this.smsService && user.phone) {
await this.smsService.send(user.phone, message);
}
// Always have a fallback notification method
await this.logNotification(user, message);
}
private async logNotification(
user: User,
message: string,
): Promise<void> {
console.log(Notification for ${user.id}: ${message});
}
}
// Module with optional providers @Module({ providers: [ NotificationService, // EMAIL_SERVICE might not be registered // SMS_SERVICE might not be registered ], exports: [NotificationService], }) export class NotificationModule {}
Property-Based Injection
import { Injectable, Inject } from '@nestjs/common';
// Property-based injection (less preferred) @Injectable() export class PaymentService { @Inject('PAYMENT_GATEWAY') private paymentGateway: PaymentGateway;
@Inject('FRAUD_DETECTOR') private fraudDetector: FraudDetector;
async processPayment( amount: number, card: CardDetails, ): Promise<PaymentResult> { const isFraudulent = await this.fraudDetector.check(card); if (isFraudulent) { throw new FraudDetectedException(); }
return await this.paymentGateway.charge(amount, card);
} }
// Constructor-based injection (preferred) @Injectable() export class OrderService { constructor( @Inject('PAYMENT_SERVICE') private readonly paymentService: PaymentService, @Inject('INVENTORY_SERVICE') private readonly inventoryService: InventoryService, ) {}
async createOrder(data: CreateOrderDto): Promise<Order> { await this.inventoryService.reserve(data.items); await this.paymentService.processPayment( data.total, data.card, );
return await this.repository.create(data);
} }
Class Provider with useClass
import { Injectable, Module } from '@nestjs/common';
// Abstract interface export abstract class LoggerService { abstract log(message: string): void; abstract error(message: string, trace: string): void; }
// Concrete implementations @Injectable() export class ConsoleLoggerService extends LoggerService { log(message: string): void { console.log(message); }
error(message: string, trace: string): void { console.error(message, trace); } }
@Injectable()
export class FileLoggerService extends LoggerService {
log(message: string): void {
fs.appendFileSync('app.log', ${message}\n);
}
error(message: string, trace: string): void {
fs.appendFileSync('error.log', ${message}\n${trace}\n);
}
}
// Use different implementations based on environment @Module({ providers: [ { provide: LoggerService, useClass: process.env.NODE_ENV === 'production' ? FileLoggerService : ConsoleLoggerService, }, ], exports: [LoggerService], }) export class LoggerModule {}
// Usage @Injectable() export class AppService { constructor(private readonly logger: LoggerService) {}
doSomething(): void { this.logger.log('Operation completed'); } }
Alias Providers (useExisting)
import { Injectable, Module } from '@nestjs/common';
@Injectable() export class UserService { findAll(): User[] { return []; } }
// Create an alias for the service @Module({ providers: [ UserService, { provide: 'IUserService', useExisting: UserService, }, { provide: 'UserRepository', useExisting: UserService, }, ], exports: [ UserService, 'IUserService', 'UserRepository', ], }) export class UserModule {}
// Can inject using any of the tokens @Injectable() export class ReportService { constructor( @Inject('IUserService') private userService: UserService, ) {}
async generateReport(): Promise<Report> { const users = this.userService.findAll(); return this.buildReport(users); } }
Module System
Module Organization and Encapsulation
import { Module } from '@nestjs/common';
// Feature module with proper encapsulation @Module({ imports: [DatabaseModule, CacheModule], providers: [ UserService, UserRepository, UserValidator, ], controllers: [UserController], exports: [UserService], // Only export what's needed }) export class UserModule {}
// Domain module grouping related features @Module({ imports: [ UserModule, AuthModule, ProfileModule, ], }) export class IdentityModule {}
// Application module @Module({ imports: [ ConfigModule.forRoot(), IdentityModule, ProductModule, OrderModule, ], }) export class AppModule {}
Global Modules with @Global()
import { Module, Global } from '@nestjs/common';
// Global module available everywhere @Global() @Module({ providers: [ { provide: 'LOGGER', useFactory: () => createLogger(), }, { provide: 'CONFIG', useValue: loadConfiguration(), }, ], exports: ['LOGGER', 'CONFIG'], }) export class CoreModule {}
// Usage in any module without importing @Injectable() export class AnyService { constructor( @Inject('LOGGER') private logger: Logger, @Inject('CONFIG') private config: Config, ) {} }
// Register global module once in AppModule @Module({ imports: [ CoreModule, // Only import once FeatureModule1, FeatureModule2, ], }) export class AppModule {}
Dynamic Modules with forRoot
import { Module, DynamicModule, Provider } from '@nestjs/common';
export interface DatabaseModuleOptions { host: string; port: number; username: string; password: string; database: string; }
@Module({}) export class DatabaseModule { static forRoot( options: DatabaseModuleOptions, ): DynamicModule { const connectionProvider: Provider = { provide: 'DATABASE_CONNECTION', useFactory: async () => { return await createConnection(options); }, };
return {
module: DatabaseModule,
providers: [
connectionProvider,
DatabaseService,
],
exports: [
'DATABASE_CONNECTION',
DatabaseService,
],
global: true,
};
} }
// Usage in AppModule @Module({ imports: [ DatabaseModule.forRoot({ host: 'localhost', port: 5432, username: 'admin', password: 'secret', database: 'myapp', }), ], }) export class AppModule {}
Dynamic Modules with forRootAsync
import { Module, DynamicModule, Provider, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config';
export interface CacheModuleAsyncOptions { useFactory: (...args: any[]) => Promise<CacheOptions>; inject?: any[]; }
@Module({}) export class CacheModule { static forRootAsync( options: CacheModuleAsyncOptions, ): DynamicModule { const cacheOptionsProvider: Provider = { provide: 'CACHE_OPTIONS', useFactory: options.useFactory, inject: options.inject || [], };
const cacheProvider: Provider = {
provide: 'CACHE_MANAGER',
useFactory: async (cacheOptions: CacheOptions) => {
return await createCacheManager(cacheOptions);
},
inject: ['CACHE_OPTIONS'],
};
return {
module: CacheModule,
providers: [
cacheOptionsProvider,
cacheProvider,
CacheService,
],
exports: ['CACHE_MANAGER', CacheService],
global: true,
};
} }
// Usage with ConfigService @Module({ imports: [ ConfigModule.forRoot(), CacheModule.forRootAsync({ useFactory: async (config: ConfigService) => ({ store: config.get('CACHE_STORE'), ttl: config.get('CACHE_TTL'), max: config.get('CACHE_MAX_ITEMS'), }), inject: [ConfigService], }), ], }) export class AppModule {}
Module Re-exporting
import { Module } from '@nestjs/common';
// Low-level infrastructure modules @Module({ providers: [DatabaseService], exports: [DatabaseService], }) export class DatabaseModule {}
@Module({ providers: [CacheService], exports: [CacheService], }) export class CacheModule {}
@Module({ providers: [QueueService], exports: [QueueService], }) export class QueueModule {}
// Shared module that re-exports common services @Module({ imports: [ DatabaseModule, CacheModule, QueueModule, ], exports: [ DatabaseModule, CacheModule, QueueModule, ], }) export class SharedModule {}
// Feature modules import SharedModule instead of individual modules @Module({ imports: [SharedModule], providers: [UserService], controllers: [UserController], }) export class UserModule {}
@Module({ imports: [SharedModule], providers: [ProductService], controllers: [ProductController], }) export class ProductModule {}
Circular Dependencies Handling
import { Module, forwardRef } from '@nestjs/common';
// UserModule depends on AuthModule @Module({ imports: [forwardRef(() => AuthModule)], providers: [UserService], exports: [UserService], }) export class UserModule {}
// AuthModule depends on UserModule @Module({ imports: [forwardRef(() => UserModule)], providers: [AuthService], exports: [AuthService], }) export class AuthModule {}
// Service-level circular dependency @Injectable() export class UserService { constructor( @Inject(forwardRef(() => AuthService)) private authService: AuthService, ) {} }
@Injectable() export class AuthService { constructor( @Inject(forwardRef(() => UserService)) private userService: UserService, ) {} }
Feature Modules with Lazy Loading
import { Module } from '@nestjs/common';
// Admin feature module @Module({ imports: [SharedModule], providers: [ AdminService, AdminGuard, ], controllers: [AdminController], }) export class AdminModule {}
// Lazy load admin module only when needed @Module({ imports: [ // Other modules loaded eagerly CoreModule, UserModule, ], }) export class AppModule {}
// In a controller or service, load AdminModule dynamically @Injectable() export class AppService { constructor(private readonly lazyModuleLoader: LazyModuleLoader) {}
async performAdminTask(): Promise<void> { const moduleRef = await this.lazyModuleLoader.load( () => AdminModule, ); const adminService = moduleRef.get(AdminService); await adminService.doAdminWork(); } }
Module Configuration Pattern
import { Module, DynamicModule } from '@nestjs/common';
export interface EmailModuleOptions { from: string; host: string; port: number; secure: boolean; }
@Module({}) export class EmailModule { static forRoot( options: EmailModuleOptions, ): DynamicModule { return { module: EmailModule, providers: [ { provide: 'EMAIL_OPTIONS', useValue: options, }, EmailService, ], exports: [EmailService], }; }
static forFeature(): DynamicModule { return { module: EmailModule, providers: [EmailTemplateService], exports: [EmailTemplateService], }; } }
// Root module configuration @Module({ imports: [ EmailModule.forRoot({ from: 'noreply@example.com', host: 'smtp.example.com', port: 587, secure: true, }), ], }) export class AppModule {}
// Feature module usage @Module({ imports: [EmailModule.forFeature()], providers: [NotificationService], }) export class NotificationModule {}
Shared Module Pattern
import { Module, Global } from '@nestjs/common';
// Shared utilities module @Global() @Module({ providers: [ DateService, StringService, ValidationService, ], exports: [ DateService, StringService, ValidationService, ], }) export class UtilsModule {}
// Shared data access module @Module({ providers: [ DataSource, TransactionManager, UnitOfWork, ], exports: [ DataSource, TransactionManager, UnitOfWork, ], }) export class DataAccessModule {}
// Combine into SharedModule @Module({ imports: [ UtilsModule, DataAccessModule, ], exports: [ UtilsModule, DataAccessModule, ], }) export class SharedModule {}
Injection Scopes
DEFAULT Scope (Singleton)
import { Injectable, Scope } from '@nestjs/common';
// Default scope - single instance shared across the app @Injectable() export class ConfigService { private config: Record<string, any>;
constructor() { this.config = this.loadConfiguration(); }
get(key: string): any { return this.config[key]; }
private loadConfiguration(): Record<string, any> { // Loaded once when application starts return { apiUrl: process.env.API_URL, dbHost: process.env.DB_HOST, }; } }
// Singleton service with state @Injectable() export class CacheService { private cache = new Map<string, any>();
set(key: string, value: any): void { this.cache.set(key, value); }
get(key: string): any { return this.cache.get(key); }
// State is shared across all requests clear(): void { this.cache.clear(); } }
REQUEST Scope with Performance Implications
import { Injectable, Scope, Inject } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Request } from 'express';
// Request-scoped provider - new instance per request @Injectable({ scope: Scope.REQUEST }) export class RequestContextService { constructor(@Inject(REQUEST) private request: Request) {}
getUserId(): string { return this.request.user?.id; }
getTenantId(): string { return this.request.headers['x-tenant-id'] as string; }
getTraceId(): string { return this.request.headers['x-trace-id'] as string; } }
// Service that depends on request-scoped provider @Injectable({ scope: Scope.REQUEST }) export class AuditService { constructor( private readonly context: RequestContextService, ) {}
async logAction(action: string): Promise<void> { await this.repository.create({ userId: this.context.getUserId(), tenantId: this.context.getTenantId(), action, timestamp: new Date(), }); } }
// Performance consideration - all consumers become request-scoped @Injectable({ scope: Scope.REQUEST }) export class UserService { // This service is now created per request // because it depends on a request-scoped service constructor( private readonly audit: AuditService, ) { console.log('New UserService instance created'); }
async createUser(data: CreateUserDto): Promise<User> { const user = await this.repository.create(data); await this.audit.logAction('user.created'); return user; } }
TRANSIENT Scope
import { Injectable, Scope } from '@nestjs/common';
// Transient scope - new instance every time it's injected @Injectable({ scope: Scope.TRANSIENT }) export class UniqueIdGenerator { private readonly id: string;
constructor() {
this.id = Math.random().toString(36).substring(7);
console.log(New generator created with id: ${this.id});
}
generate(): string {
return ${this.id}-${Date.now()};
}
}
// Each injection point gets its own instance @Injectable() export class OrderService { constructor( private readonly idGen1: UniqueIdGenerator, // Instance 1 ) {} }
@Injectable() export class InvoiceService { constructor( private readonly idGen2: UniqueIdGenerator, // Instance 2 ) {} }
// Transient for stateful operations @Injectable({ scope: Scope.TRANSIENT }) export class QueryBuilder { private conditions: string[] = []; private params: any[] = [];
where(condition: string, ...params: any[]): this { this.conditions.push(condition); this.params.push(...params); return this; }
build(): { query: string; params: any[] } {
return {
query: SELECT * FROM table WHERE ${this.conditions.join(' AND ')},
params: this.params,
};
}
}
Durable Providers
import { Injectable, Scope } from '@nestjs/common';
// Durable provider - survives across requests @Injectable({ scope: Scope.DEFAULT, durable: true }) export class ConnectionPoolService { private pool: Pool;
constructor() { this.pool = createPool({ host: 'localhost', port: 5432, max: 20, }); }
getConnection(): Promise<Connection> { return this.pool.connect(); }
async onModuleDestroy(): Promise<void> { await this.pool.end(); } }
// Durable request-scoped provider @Injectable({ scope: Scope.REQUEST, durable: true, }) export class RequestLoggerService { private logs: string[] = [];
log(message: string): void { this.logs.push(message); }
getLogs(): string[] { return this.logs; } }
Scope Inheritance
import { Injectable, Scope } from '@nestjs/common';
// Parent service with DEFAULT scope @Injectable() export class DatabaseService { query(sql: string): Promise<any> { return this.pool.query(sql); } }
// Child service inherits scope from parent @Injectable() export class UserRepository { constructor(private readonly db: DatabaseService) {}
findAll(): Promise<User[]> { return this.db.query('SELECT * FROM users'); } }
// Request-scoped parent @Injectable({ scope: Scope.REQUEST }) export class RequestContext { constructor(@Inject(REQUEST) private request: Request) {}
getTenantId(): string { return this.request.headers['x-tenant-id'] as string; } }
// Child inherits REQUEST scope @Injectable({ scope: Scope.REQUEST }) export class TenantService { constructor( private readonly context: RequestContext, private readonly db: DatabaseService, // Still singleton ) {}
async getData(): Promise<any[]> {
const tenantId = this.context.getTenantId();
return this.db.query(
SELECT * FROM data WHERE tenant_id = '${tenantId}',
);
}
}
Scope Configuration in Modules
import { Module } from '@nestjs/common';
@Module({ providers: [ // Default scope (singleton) ConfigService,
// Request scope
{
provide: 'REQUEST_LOGGER',
scope: Scope.REQUEST,
useClass: RequestLoggerService,
},
// Transient scope
{
provide: 'ID_GENERATOR',
scope: Scope.TRANSIENT,
useClass: IdGeneratorService,
},
], }) export class AppModule {}
Advanced Patterns
Custom Decorators for Injection
import { Inject } from '@nestjs/common';
// Custom decorator for logger injection export const InjectLogger = () => Inject('LOGGER');
// Custom decorator for repository injection
export function InjectRepository(
entity: Function,
): ParameterDecorator {
return Inject(${entity.name}Repository);
}
// Custom decorator with options
export function InjectCache(
namespace?: string,
): ParameterDecorator {
const token = namespace ? CACHE:${namespace} : 'CACHE';
return Inject(token);
}
// Usage in services @Injectable() export class UserService { constructor( @InjectLogger() private logger: Logger, @InjectRepository(User) private repo: Repository<User>, @InjectCache('users') private cache: CacheManager, ) {}
async findAll(): Promise<User[]> { this.logger.log('Finding all users');
const cached = await this.cache.get('all');
if (cached) return cached;
const users = await this.repo.find();
await this.cache.set('all', users, 300);
return users;
} }
// Register providers with custom tokens @Module({ providers: [ { provide: 'LOGGER', useFactory: () => createLogger(), }, { provide: 'UserRepository', useClass: UserRepository, }, { provide: 'CACHE:users', useFactory: () => createCacheManager('users'), }, ], }) export class UserModule {}
Provider Arrays and Multi-Providers
import { Module, Inject } from '@nestjs/common';
// Define token for array of providers export const EVENT_HANDLERS = 'EVENT_HANDLERS';
// Multiple implementations of event handler @Injectable() export class UserEventHandler implements EventHandler { handle(event: Event): void { console.log('User event:', event); } }
@Injectable() export class AuditEventHandler implements EventHandler { handle(event: Event): void { console.log('Audit event:', event); } }
@Injectable() export class NotificationEventHandler implements EventHandler { handle(event: Event): void { console.log('Notification event:', event); } }
// Module registering multiple providers @Module({ providers: [ UserEventHandler, AuditEventHandler, NotificationEventHandler, { provide: EVENT_HANDLERS, useFactory: ( userHandler: UserEventHandler, auditHandler: AuditEventHandler, notificationHandler: NotificationEventHandler, ) => [userHandler, auditHandler, notificationHandler], inject: [ UserEventHandler, AuditEventHandler, NotificationEventHandler, ], }, ], exports: [EVENT_HANDLERS], }) export class EventModule {}
// Service using array of providers @Injectable() export class EventBus { constructor( @Inject(EVENT_HANDLERS) private handlers: EventHandler[], ) {}
emit(event: Event): void { this.handlers.forEach((handler) => { handler.handle(event); }); } }
Lazy Module Loading
import { Injectable, LazyModuleLoader, } from '@nestjs/common';
@Injectable() export class ReportService { constructor( private readonly lazyModuleLoader: LazyModuleLoader, ) {}
async generateComplexReport(): Promise<Report> { // Load heavy module only when needed const moduleRef = await this.lazyModuleLoader.load( () => import('./analytics/analytics.module') .then((m) => m.AnalyticsModule), );
const analyticsService = moduleRef.get(AnalyticsService);
const data = await analyticsService.analyze();
return this.buildReport(data);
}
async exportToPdf(report: Report): Promise<Buffer> { // Load PDF module lazily const moduleRef = await this.lazyModuleLoader.load( () => import('./pdf/pdf.module') .then((m) => m.PdfModule), );
const pdfService = moduleRef.get(PdfService);
return await pdfService.generate(report);
} }
Testing with Dependency Injection
import { Test, TestingModule } from '@nestjs/testing';
describe('UserService', () => { let service: UserService; let repository: Repository<User>;
beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, { provide: 'UserRepository', useValue: { find: jest.fn(), findOne: jest.fn(), create: jest.fn(), save: jest.fn(), }, }, { provide: 'LOGGER', useValue: { log: jest.fn(), error: jest.fn(), }, }, ], }).compile();
service = module.get<UserService>(UserService);
repository = module.get('UserRepository');
});
it('should find all users', async () => { const users = [{ id: '1', name: 'John' }]; jest.spyOn(repository, 'find').mockResolvedValue(users);
const result = await service.findAll();
expect(result).toEqual(users);
expect(repository.find).toHaveBeenCalledTimes(1);
});
it('should create a user', async () => { const createDto = { name: 'Jane', email: 'jane@example.com' }; const user = { id: '2', ...createDto };
jest.spyOn(repository, 'create').mockReturnValue(user);
jest.spyOn(repository, 'save').mockResolvedValue(user);
const result = await service.create(createDto);
expect(result).toEqual(user);
expect(repository.create).toHaveBeenCalledWith(createDto);
expect(repository.save).toHaveBeenCalledWith(user);
}); });
// Integration testing with test database describe('UserService (integration)', () => { let app: INestApplication; let service: UserService;
beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [ DatabaseModule.forRoot({ type: 'postgres', host: 'localhost', port: 5433, database: 'test', }), UserModule, ], }).compile();
app = moduleRef.createNestApplication();
await app.init();
service = moduleRef.get<UserService>(UserService);
});
afterAll(async () => { await app.close(); });
it('should persist user to database', async () => { const createDto = { name: 'Integration', email: 'test@test.com' }; const user = await service.create(createDto);
expect(user.id).toBeDefined();
expect(user.name).toBe(createDto.name);
const found = await service.findById(user.id);
expect(found).toEqual(user);
}); });
ModuleRef for Dynamic Provider Resolution
import { Injectable, ModuleRef } from '@nestjs/core';
@Injectable() export class DynamicService { constructor(private readonly moduleRef: ModuleRef) {}
async processWithStrategy(
strategyName: string,
data: any,
): Promise<any> {
// Dynamically resolve provider at runtime
const strategy = this.moduleRef.get(
${strategyName}Strategy,
{ strict: false },
);
return await strategy.process(data);
}
async getServiceByTenant(tenantId: string): Promise<any> {
// Get service instance dynamically
const token = TenantService:${tenantId};
try {
return this.moduleRef.get(token, { strict: false });
} catch {
// Fallback to default service
return this.moduleRef.get('DefaultTenantService');
}
}
}
// Using ModuleRef with request-scoped providers @Injectable({ scope: Scope.REQUEST }) export class ContextAwareService { constructor(private readonly moduleRef: ModuleRef) {}
async getCurrentUser(): Promise<User> { // Get request-scoped context const context = await this.moduleRef.resolve( RequestContextService, ); const userId = context.getUserId();
const userService = this.moduleRef.get(UserService);
return await userService.findById(userId);
} }
Plugin Pattern with Dependency Injection
import { Module, DynamicModule, Type } from '@nestjs/common';
export interface Plugin { name: string; initialize(): Promise<void>; execute(data: any): Promise<any>; }
export interface PluginModuleOptions { plugins: Type<Plugin>[]; }
@Module({}) export class PluginModule { static forRoot( options: PluginModuleOptions, ): DynamicModule { const pluginProviders = options.plugins.map((plugin) => ({ provide: plugin, useClass: plugin, }));
const pluginRegistryProvider = {
provide: 'PLUGIN_REGISTRY',
useFactory: (...plugins: Plugin[]) => plugins,
inject: options.plugins,
};
return {
module: PluginModule,
providers: [
...pluginProviders,
pluginRegistryProvider,
PluginExecutor,
],
exports: [PluginExecutor],
};
} }
// Plugin implementations @Injectable() export class ValidationPlugin implements Plugin { name = 'validation';
async initialize(): Promise<void> { console.log('Validation plugin initialized'); }
async execute(data: any): Promise<any> { // Validate data return data; } }
@Injectable() export class TransformPlugin implements Plugin { name = 'transform';
async initialize(): Promise<void> { console.log('Transform plugin initialized'); }
async execute(data: any): Promise<any> { // Transform data return data; } }
// Plugin executor @Injectable() export class PluginExecutor { constructor( @Inject('PLUGIN_REGISTRY') private plugins: Plugin[], ) {}
async executeAll(data: any): Promise<any> { let result = data; for (const plugin of this.plugins) { result = await plugin.execute(result); } return result; } }
// Usage @Module({ imports: [ PluginModule.forRoot({ plugins: [ValidationPlugin, TransformPlugin], }), ], }) export class AppModule {}
Conditional Provider Registration
import { Module, DynamicModule } from '@nestjs/common';
@Module({}) export class StorageModule { static forRoot(): DynamicModule { const providers = [];
// Conditional provider based on environment
if (process.env.STORAGE_TYPE === 's3') {
providers.push({
provide: 'STORAGE_SERVICE',
useClass: S3StorageService,
});
} else if (process.env.STORAGE_TYPE === 'gcs') {
providers.push({
provide: 'STORAGE_SERVICE',
useClass: GcsStorageService,
});
} else {
providers.push({
provide: 'STORAGE_SERVICE',
useClass: LocalStorageService,
});
}
// Conditional feature providers
if (process.env.ENABLE_COMPRESSION === 'true') {
providers.push(CompressionService);
}
if (process.env.ENABLE_ENCRYPTION === 'true') {
providers.push(EncryptionService);
}
return {
module: StorageModule,
providers,
exports: ['STORAGE_SERVICE'],
};
} }
Best Practices
Use constructor injection over property injection: Constructor injection makes dependencies explicit, ensures they're available when the class is instantiated, and works better with TypeScript's type system.
Prefer class-based providers for services: Class providers are more idiomatic in NestJS, provide better type safety, and integrate seamlessly with decorators like @Injectable().
Use factory providers for complex initialization: When providers need async initialization, depend on other services, or require conditional logic, factory providers offer the flexibility needed.
Avoid circular dependencies with forwardRef: While forwardRef() solves circular dependencies, it's better to restructure your modules to eliminate the circular reference entirely.
Keep modules focused and cohesive: Each module should represent a single feature or domain. This improves maintainability, makes testing easier, and enables better code organization.
Use dynamic modules for configurable features: When building reusable modules that need configuration, implement forRoot() and forRootAsync() methods to provide flexible initialization.
Leverage REQUEST scope only when needed: Request-scoped providers have performance overhead. Use them only when you truly need per-request state, like request context or tenant isolation.
Use symbol tokens for better type safety: Symbol tokens prevent naming conflicts and provide better IntelliSense support compared to string tokens.
Export only what's needed from modules: Keep module interfaces minimal by exporting only the providers that other modules need to use. This maintains encapsulation and reduces coupling.
Test providers in isolation: Write unit tests that mock dependencies to test providers in isolation. Use integration tests to verify the full dependency graph works correctly.
Common Pitfalls
Circular dependency errors: Occurs when Module A imports Module B, and Module B imports Module A. Restructure your modules or use forwardRef() as a last resort.
REQUEST scope performance overhead: Request-scoped providers are created for every request, which adds memory and CPU overhead. All dependent providers also become request-scoped.
Not handling async provider initialization: Forgetting to use async/await in factory providers can lead to providers being injected before they're fully initialized.
Overusing global modules: Global modules are convenient but can lead to tight coupling. Use them sparingly for truly global services like logging and configuration.
Missing provider exports in modules: If a provider is not listed in the module's exports array, it won't be available to other modules that import it.
Token name conflicts: Using generic string tokens like 'config' or 'service' across multiple modules can cause conflicts. Use descriptive, namespaced tokens.
Memory leaks with REQUEST scope: Request-scoped providers that hold references to large objects or don't clean up resources can cause memory leaks over time.
Not cleaning up resources in onModuleDestroy: Providers that create connections, timers, or other resources should implement onModuleDestroy to clean up properly.
Tight coupling between modules: Importing too many modules or depending on internal implementation details creates tight coupling that makes refactoring difficult.
Missing @Injectable() decorator: Forgetting to add @Injectable() to a class that should be a provider results in runtime errors when NestJS tries to inject it.
Resources
-
NestJS Dependency Injection
-
NestJS Modules
-
NestJS Injection Scopes
-
NestJS Dynamic Modules
-
NestJS Testing
-
NestJS Circular Dependency
-
NestJS Module Reference
-
NestJS Lifecycle Events