Lifecycle Events trong NestJS
Lifecycle Events cho phép bạn hook vào các sự kiện quan trọng trong vòng đời của ứng dụng NestJS. Từ khởi tạo modules cho đến shutdown toàn bộ ứng dụng, NestJS cung cấp lifecycle hooks để bạn thực thi custom logic tại các thời điểm chính xác.
Các Lifecycle Hooks
NestJS cung cấp 8 lifecycle hooks chính:
1. OnModuleInit
Được gọi sau khi module được khởi tạo:
import { Injectable, OnModuleInit } from '@nestjs/common';
@Injectable()
export class DatabaseService implements OnModuleInit {
async onModuleInit() {
console.log('Module initialized - connecting to database...');
await this.connect();
}
private async connect() {
// Simulate database connection
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('✓ Database connected');
}
}
// Module
@Module({
providers: [DatabaseService],
})
export class DatabaseModule {}
// Thứ tự: Provider instantiation → onModuleInit() → Ready to use
Khi sử dụng:
- Khởi tạo kết nối database
- Load configuration
- Setup caching
- Initialize external services
2. OnApplicationBootstrap
Được gọi sau khi tất cả modules đã khởi tạo và ứng dụng sẵn sàng:
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
@Injectable()
export class AppService implements OnApplicationBootstrap {
constructor(private database: DatabaseService) {}
async onApplicationBootstrap() {
console.log('Application bootstrapped - running migrations...');
await this.runMigrations();
console.log('✓ Migrations completed');
}
private async runMigrations() {
// Run database migrations
await new Promise(resolve => setTimeout(resolve, 500));
}
}
Khi sử dụng:
- Chạy migrations
- Seed initial data
- Start background jobs
- Perform startup checks
3. OnModuleDestroy
Được gọi khi module bị hủy (ứng dụng tắt):
import { Injectable, OnModuleDestroy } from '@nestjs/common';
@Injectable()
export class DatabaseService implements OnModuleDestroy {
async onModuleDestroy() {
console.log('Module destroying - closing database connection...');
await this.close();
}
private async close() {
// Close database connection
await new Promise(resolve => setTimeout(resolve, 500));
console.log('✓ Database connection closed');
}
}
Khi sử dụng:
- Đóng database connections
- Release file handles
- Cleanup temporary resources
- Save state
4. BeforeApplicationShutdown
Được gọi trước khi ứng dụng tắt hoàn toàn:
import { Injectable, BeforeApplicationShutdown } from '@nestjs/common';
@Injectable()
export class GracefulShutdownService implements BeforeApplicationShutdown {
async beforeApplicationShutdown(signal?: string) {
console.log(`Received signal: ${signal}`);
console.log('Cleaning up before shutdown...');
// Cleanup logic
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('✓ Cleanup completed');
}
}
Khi sử dụng:
- Graceful shutdown
- Save data before exit
- Send notifications
- Final cleanup
5. OnModuleInit (Async)
Async version của OnModuleInit:
@Injectable()
export class AsyncInitService implements OnModuleInit {
private initialized = false;
async onModuleInit() {
console.log('Starting async initialization...');
// Async operations
await this.loadConfiguration();
await this.establishConnections();
await this.validateSetup();
this.initialized = true;
console.log('✓ Async initialization completed');
}
private async loadConfiguration() {
await new Promise(resolve => setTimeout(resolve, 500));
}
private async establishConnections() {
await new Promise(resolve => setTimeout(resolve, 500));
}
private async validateSetup() {
await new Promise(resolve => setTimeout(resolve, 500));
}
isInitialized() {
return this.initialized;
}
}
6. OnApplicationShutdown
Được gọi cuối cùng trước khi process kết thúc:
import { Injectable, OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class AppShutdownService implements OnApplicationShutdown {
onApplicationShutdown(signal?: string) {
console.log(`Application shutting down with signal: ${signal}`);
// Sync cleanup - không có time để async
}
}
Lifecycle Sequence
Application Start
↓
1. Constructor của providers
↓
2. onModuleInit() - trong từng module
↓
3. onApplicationBootstrap() - khi tất cả modules ready
↓
4. Application Running & Accepting Requests
↓
(SIGTERM/SIGKILL received)
↓
5. beforeApplicationShutdown() - graceful shutdown
↓
6. onModuleDestroy() - trong từng module
↓
7. onApplicationShutdown() - final cleanup
↓
Process Exit
Ví Dụ Lifecycle Events Thực Tế
1. Database Connection Management
// database.service.ts
@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
private connection: any;
private isConnected = false;
async onModuleInit() {
console.log('Initializing database connection...');
try {
this.connection = await this.createConnection();
this.isConnected = true;
console.log('✓ Database connected');
} catch (error) {
console.error('✗ Failed to connect to database:', error.message);
throw error;
}
}
async onModuleDestroy() {
console.log('Closing database connection...');
try {
if (this.connection) {
await this.connection.close();
this.isConnected = false;
console.log('✓ Database disconnected');
}
} catch (error) {
console.error('✗ Error closing database:', error.message);
}
}
private async createConnection() {
// Simulate connection creation
return {
close: async () => {
await new Promise(resolve => setTimeout(resolve, 500));
},
};
}
isConnected() {
return this.isConnected;
}
async query(sql: string) {
if (!this.isConnected) {
throw new Error('Database not connected');
}
// Execute query
return [];
}
}
2. Cache Initialization and Cleanup
// cache.service.ts
@Injectable()
export class CacheService implements OnModuleInit, OnModuleDestroy {
private cache: Map<string, any> = new Map();
private cleanupInterval: NodeJS.Timeout;
onModuleInit() {
console.log('Initializing cache service...');
// Start cache cleanup interval
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, 60000); // Every minute
console.log('✓ Cache service initialized');
}
onModuleDestroy() {
console.log('Destroying cache service...');
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
this.cache.clear();
console.log('✓ Cache cleared');
}
set(key: string, value: any, ttl?: number) {
this.cache.set(key, {
value,
expiresAt: ttl ? Date.now() + ttl * 1000 : null,
});
}
get(key: string) {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
if (entry.expiresAt && Date.now() > entry.expiresAt) {
this.cache.delete(key);
return null;
}
return entry.value;
}
private cleanup() {
const now = Date.now();
let cleaned = 0;
for (const [key, entry] of this.cache.entries()) {
if (entry.expiresAt && now > entry.expiresAt) {
this.cache.delete(key);
cleaned++;
}
}
if (cleaned > 0) {
console.log(`[Cache] Cleaned up ${cleaned} expired entries`);
}
}
}
3. Migration Runner
// migration.service.ts
@Injectable()
export class MigrationService implements OnApplicationBootstrap {
constructor(private database: DatabaseService) {}
async onApplicationBootstrap() {
console.log('Running database migrations...');
try {
await this.runMigrations();
console.log('✓ All migrations completed');
} catch (error) {
console.error('✗ Migration failed:', error.message);
throw error;
}
}
private async runMigrations() {
const migrations = [
'001_create_users_table',
'002_create_posts_table',
'003_add_indexes',
];
for (const migration of migrations) {
console.log(`Running: ${migration}`);
await new Promise(resolve => setTimeout(resolve, 200));
console.log(`✓ ${migration} completed`);
}
}
}
4. Health Check Service
// health-check.service.ts
@Injectable()
export class HealthCheckService implements OnApplicationBootstrap {
constructor(
private database: DatabaseService,
private cache: CacheService,
) {}
async onApplicationBootstrap() {
console.log('Running startup health checks...');
try {
await this.checkDatabase();
await this.checkCache();
console.log('✓ All health checks passed');
} catch (error) {
console.error('✗ Health check failed:', error.message);
throw error;
}
}
private async checkDatabase() {
console.log(' Checking database...');
if (!this.database.isConnected()) {
throw new Error('Database is not connected');
}
console.log(' ✓ Database OK');
}
private async checkCache() {
console.log(' Checking cache...');
// Set and get a test value
this.cache.set('health_check', 'ok');
const value = this.cache.get('health_check');
if (value !== 'ok') {
throw new Error('Cache health check failed');
}
console.log(' ✓ Cache OK');
}
}
5. Background Job Scheduler
// scheduler.service.ts
@Injectable()
export class SchedulerService implements OnApplicationBootstrap, OnModuleDestroy {
private jobs: Map<string, NodeJS.Timeout> = new Map();
async onApplicationBootstrap() {
console.log('Starting job scheduler...');
// Schedule jobs
this.scheduleJob('cleanup-logs', this.cleanupLogs.bind(this), 3600000); // Every hour
this.scheduleJob('sync-cache', this.syncCache.bind(this), 300000); // Every 5 minutes
this.scheduleJob('health-check', this.healthCheck.bind(this), 60000); // Every minute
console.log('✓ Job scheduler started');
}
onModuleDestroy() {
console.log('Stopping job scheduler...');
for (const [name, timeout] of this.jobs.entries()) {
clearInterval(timeout);
console.log(` Stopped job: ${name}`);
}
this.jobs.clear();
}
private scheduleJob(name: string, fn: () => void, interval: number) {
console.log(` Scheduled job: ${name} (interval: ${interval}ms)`);
const timeout = setInterval(() => {
fn();
}, interval);
this.jobs.set(name, timeout);
}
private cleanupLogs() {
console.log('[Job] Cleaning up old logs...');
}
private syncCache() {
console.log('[Job] Syncing cache...');
}
private healthCheck() {
console.log('[Job] Running health check...');
}
}
6. Configuration Loading
// config-loader.service.ts
@Injectable()
export class ConfigLoaderService implements OnModuleInit {
private config: any;
async onModuleInit() {
console.log('Loading application configuration...');
try {
this.config = await this.loadConfig();
this.validateConfig();
console.log('✓ Configuration loaded and validated');
} catch (error) {
console.error('✗ Failed to load configuration:', error.message);
throw error;
}
}
private async loadConfig() {
// Load from file, environment, or external service
return {
app: {
name: process.env.APP_NAME || 'MyApp',
version: '1.0.0',
env: process.env.NODE_ENV || 'development',
},
database: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
},
cache: {
enabled: process.env.CACHE_ENABLED === 'true',
ttl: 3600,
},
};
}
private validateConfig() {
if (!this.config.app.name) {
throw new Error('Application name is required');
}
if (!this.config.database.host) {
throw new Error('Database host is required');
}
}
get(path: string) {
return this.getNestedValue(this.config, path.split('.'));
}
private getNestedValue(obj: any, keys: string[]): any {
let value = obj;
for (const key of keys) {
value = value[key];
if (value === undefined) {
return undefined;
}
}
return value;
}
}
7. Logger Initialization
// logger.service.ts
@Injectable()
export class LoggerService implements OnModuleInit, OnModuleDestroy {
private logStream: any;
private logFile = './logs/app.log';
onModuleInit() {
console.log('Initializing logger...');
try {
// Create log directory if needed
// Open log file for writing
this.logStream = {
write: (message: string) => {
console.log(message);
},
};
console.log('✓ Logger initialized');
} catch (error) {
console.error('✗ Failed to initialize logger:', error.message);
}
}
onModuleDestroy() {
console.log('Closing logger...');
if (this.logStream) {
// Close log stream
this.logStream = null;
console.log('✓ Logger closed');
}
}
log(message: string) {
if (this.logStream) {
this.logStream.write(`[${new Date().toISOString()}] ${message}\n`);
}
}
}
8. Graceful Shutdown Handler
// graceful-shutdown.service.ts
@Injectable()
export class GracefulShutdownService implements BeforeApplicationShutdown {
private activeRequests = 0;
private isShuttingDown = false;
constructor(private logger: LoggerService) {}
trackRequest() {
if (this.isShuttingDown) {
return false; // Reject new requests during shutdown
}
this.activeRequests++;
return true;
}
releaseRequest() {
this.activeRequests--;
}
async beforeApplicationShutdown(signal?: string) {
console.log(`\nReceived shutdown signal: ${signal}`);
this.isShuttingDown = true;
// Wait for active requests to complete
console.log('Waiting for active requests to complete...');
let waitTime = 0;
const maxWaitTime = 30000; // 30 seconds
while (this.activeRequests > 0 && waitTime < maxWaitTime) {
console.log(` Active requests: ${this.activeRequests}`);
await new Promise(resolve => setTimeout(resolve, 1000));
waitTime += 1000;
}
if (this.activeRequests > 0) {
console.warn(` ✗ Timeout waiting for requests (${this.activeRequests} still active)`);
} else {
console.log(' ✓ All requests completed');
}
// Close connections
console.log('Closing connections...');
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(' ✓ Connections closed');
console.log('✓ Graceful shutdown completed');
}
}
9. Complete Application Lifecycle
// Complete example with all lifecycle hooks
@Injectable()
export class ServiceA implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
console.log('ServiceA: onModuleInit');
}
async onModuleDestroy() {
console.log('ServiceA: onModuleDestroy');
}
}
@Injectable()
export class ServiceB implements OnModuleInit {
constructor(private serviceA: ServiceA) {}
async onModuleInit() {
console.log('ServiceB: onModuleInit (depends on ServiceA)');
}
}
@Injectable()
export class AppService implements OnApplicationBootstrap, BeforeApplicationShutdown {
constructor(
private serviceA: ServiceA,
private serviceB: ServiceB,
) {}
async onApplicationBootstrap() {
console.log('AppService: onApplicationBootstrap (all modules ready)');
}
async beforeApplicationShutdown(signal?: string) {
console.log(`AppService: beforeApplicationShutdown (signal: ${signal})`);
}
}
// Output khi ứng dụng start:
// ServiceA: onModuleInit
// ServiceB: onModuleInit (depends on ServiceA)
// AppService: onApplicationBootstrap (all modules ready)
// ✓ Application is now running
// Output khi ứng dụng shutdown:
// Received shutdown signal: SIGTERM
// AppService: beforeApplicationShutdown (signal: SIGTERM)
// ServiceA: onModuleDestroy
// ✓ Application shutdown complete
Best Practices
1. Error Handling in Lifecycle Hooks
// ✅ Đúng - Handle errors gracefully
@Injectable()
export class SafeService implements OnModuleInit {
async onModuleInit() {
try {
await this.initialize();
} catch (error) {
console.error('Initialization failed:', error);
// Optionally exit process
process.exit(1);
}
}
private async initialize() {
// Initialization logic
}
}
// ❌ Sai - Unhandled errors
@Injectable()
export class BadService implements OnModuleInit {
async onModuleInit() {
await this.initialize(); // Error will crash app
}
}
2. Implement Proper Cleanup
// ✅ Đúng - Clean up all resources
@Injectable()
export class CleanService implements OnModuleDestroy {
private connections: any[] = [];
private timers: NodeJS.Timeout[] = [];
async onModuleDestroy() {
// Close all connections
for (const conn of this.connections) {
await conn.close();
}
// Clear all timers
for (const timer of this.timers) {
clearInterval(timer);
}
console.log('✓ All resources cleaned up');
}
}
3. Log Lifecycle Events
// ✅ Đúng - Log for debugging
@Injectable()
export class LoggedService implements OnModuleInit, OnModuleDestroy {
private logger = new Logger('LoggedService');
async onModuleInit() {
this.logger.log('Initializing...');
// Initialization
this.logger.log('Initialization complete');
}
async onModuleDestroy() {
this.logger.log('Destroying...');
// Cleanup
this.logger.log('Cleanup complete');
}
}
4. Order Dependencies Properly
// ✅ Đúng - Consider initialization order
@Module({
providers: [
ConfigService, // Initialize first
DatabaseService, // Depends on ConfigService
CacheService, // Depends on ConfigService
AppService, // Depends on Database and Cache
],
})
export class AppModule {}
// ❌ Sai - Random order
@Module({
providers: [
AppService, // Might initialize before dependencies
DatabaseService,
ConfigService,
],
})
export class BadModule {}
5. Use Signals for Graceful Shutdown
// ✅ Đúng - Handle shutdown signals
@Injectable()
export class ShutdownHandler implements BeforeApplicationShutdown {
async beforeApplicationShutdown(signal?: string) {
console.log(`Shutting down with signal: ${signal}`);
// Handle different signals
switch (signal) {
case 'SIGTERM':
// Graceful shutdown
break;
case 'SIGKILL':
// Immediate shutdown
break;
}
}
}
Complete Example
// app.service.ts
@Injectable()
export class AppService
implements OnModuleInit, OnApplicationBootstrap, BeforeApplicationShutdown, OnModuleDestroy {
private logger = new Logger('AppService');
private isRunning = false;
async onModuleInit() {
this.logger.log('Module initialization started');
try {
// Initialize
await this.setupResources();
this.logger.log('✓ Module initialization completed');
} catch (error) {
this.logger.error('Module initialization failed:', error.message);
throw error;
}
}
async onApplicationBootstrap() {
this.logger.log('Application bootstrap started');
try {
// Application startup
await this.runMigrations();
await this.seedData();
this.isRunning = true;
this.logger.log('✓ Application bootstrap completed');
} catch (error) {
this.logger.error('Application bootstrap failed:', error.message);
throw error;
}
}
async beforeApplicationShutdown(signal?: string) {
this.logger.log(`Shutdown signal received: ${signal}`);
this.isRunning = false;
try {
await this.gracefulShutdown();
this.logger.log('✓ Graceful shutdown completed');
} catch (error) {
this.logger.error('Graceful shutdown error:', error.message);
}
}
async onModuleDestroy() {
this.logger.log('Module destruction started');
try {
await this.cleanupResources();
this.logger.log('✓ Module destruction completed');
} catch (error) {
this.logger.error('Module destruction error:', error.message);
}
}
private async setupResources() {
await new Promise(resolve => setTimeout(resolve, 100));
}
private async runMigrations() {
await new Promise(resolve => setTimeout(resolve, 100));
}
private async seedData() {
await new Promise(resolve => setTimeout(resolve, 100));
}
private async gracefulShutdown() {
await new Promise(resolve => setTimeout(resolve, 100));
}
private async cleanupResources() {
await new Promise(resolve => setTimeout(resolve, 100));
}
isAppRunning() {
return this.isRunning;
}
}
Kết Luận
Lifecycle Events là quan trọng để:
- Initialize resources một cách an toàn
- Perform startup checks trước khi chấp nhận requests
- Clean up properly khi ứng dụng tắt
- Implement graceful shutdown
- Run migrations và seed data
Key Hooks:
- OnModuleInit - Module initialization
- OnApplicationBootstrap - All modules ready
- BeforeApplicationShutdown - Graceful shutdown
- OnModuleDestroy - Resource cleanup
Sử dụng lifecycle events đúng cách là essential để xây dựng robust, production-grade NestJS applications!