| name | nestjs-dependency-injection |
| description | Use when nestJS dependency injection with providers, modules, and decorators. Use when building modular NestJS applications. |
| allowed-tools | Bash, Read, Write, Edit, Glob, Grep |
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.