Asynchronous Providers trong NestJS
Asynchronous Providers cho phép bạn thực hiện các tác vụ asynchronous (như kết nối database, load configuration từ API, v.v.) trước khi ứng dụng khởi động. NestJS sẽ đợi tất cả async providers hoàn thành trước khi ứng dụng sẵn sàng phục vụ requests.
Khái Niệm Async Provider
Async Provider là một provider mà việc khởi tạo trả về một Promise thay vì giá trị trực tiếp. NestJS sẽ tự động chờ Promise resolve trước khi tiếp tục.
// Synchronous Provider
@Module({
providers: [DatabaseService],
})
export class AppModule {}
// Asynchronous Provider
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async () => {
// Tác vụ async
const connection = await createDatabaseConnection();
return connection;
},
},
],
})
export class AppModule {}
Các Cách Tạo Async Provider
1. useFactory with Async Function
// database.service.ts
export interface DatabaseConnection {
query(sql: string): Promise<any>;
close(): Promise<void>;
}
async function createDatabaseConnection(): Promise<DatabaseConnection> {
console.log('Connecting to database...');
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 1000));
return {
query: async (sql: string) => {
console.log(`Executing: ${sql}`);
return [];
},
close: async () => {
console.log('Database connection closed');
},
};
}
// database.module.ts
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: createDatabaseConnection,
},
],
exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}
// Sử dụng
@Injectable()
export class UsersService {
constructor(
@Inject('DATABASE_CONNECTION')
private database: DatabaseConnection,
) {}
async getUsers() {
return this.database.query('SELECT * FROM users');
}
}
2. useFactory with Dependencies
// config.service.ts
@Injectable()
export class ConfigService {
get(key: string): string {
return process.env[key];
}
}
// redis.service.ts
@Injectable()
export class RedisService {
private client: any;
constructor(private configService: ConfigService) {}
async connect(): Promise<void> {
const redisUrl = this.configService.get('REDIS_URL');
console.log(`Connecting to Redis: ${redisUrl}`);
await new Promise(resolve => setTimeout(resolve, 500));
this.client = { connected: true };
}
async get(key: string) {
return this.client.get(key);
}
}
// redis.module.ts
@Module({
providers: [
{
provide: 'REDIS_SERVICE',
useFactory: async (configService: ConfigService) => {
const redisService = new RedisService(configService);
await redisService.connect();
return redisService;
},
inject: [ConfigService],
},
],
exports: ['REDIS_SERVICE'],
})
export class RedisModule {}
3. useClass with Async Initialization
@Injectable()
export class DatabaseService {
private connection: any;
constructor(private configService: ConfigService) {}
async onModuleInit() {
// Called after provider is instantiated
await this.connect();
}
private async connect() {
const dbUrl = this.configService.get('DATABASE_URL');
console.log(`Connecting to: ${dbUrl}`);
await new Promise(resolve => setTimeout(resolve, 1000));
this.connection = { connected: true };
}
async query(sql: string) {
return this.connection.query(sql);
}
}
@Module({
providers: [
ConfigService,
DatabaseService, // Automatically calls onModuleInit
],
})
export class DatabaseModule {}
4. useValue with Promise
// config.module.ts
@Module({
providers: [
{
provide: 'APP_CONFIG',
useValue: (async () => {
console.log('Loading configuration...');
await new Promise(resolve => setTimeout(resolve, 500));
return {
port: 3000,
database: 'postgres://...',
logLevel: 'info',
};
})(),
},
],
exports: ['APP_CONFIG'],
})
export class ConfigModule {}
Ví Dụ Async Providers Thực Tế
1. Database Connection Pool
// database.config.ts
export interface DatabaseConfig {
host: string;
port: number;
username: string;
password: string;
database: string;
maxConnections: number;
}
// database.connection.ts
export class DatabaseConnection {
private pool: any;
private activeConnections = 0;
async initialize(config: DatabaseConfig): Promise<void> {
console.log(`Initializing database pool with max ${config.maxConnections} connections`);
// Simulate connection pool creation
await new Promise(resolve => setTimeout(resolve, 1000));
this.pool = {
config,
connections: [],
};
console.log('Database pool initialized successfully');
}
async query(sql: string): Promise<any> {
if (!this.pool) {
throw new Error('Database not initialized');
}
return { rows: [] };
}
async close(): Promise<void> {
console.log('Closing database connections');
this.pool = null;
}
getStatus() {
return {
initialized: !!this.pool,
activeConnections: this.activeConnections,
};
}
}
// database.module.ts
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
const config = configService.get<DatabaseConfig>('database');
const connection = new DatabaseConnection();
await connection.initialize(config);
return connection;
},
inject: [ConfigService],
},
],
exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}
2. Configuration Loading from External API
// config.loader.ts
export class ConfigLoader {
async loadFromAPI(apiUrl: string): Promise<Record<string, any>> {
console.log(`Loading configuration from ${apiUrl}...`);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
return {
appName: 'MyApp',
version: '1.0.0',
features: {
auth: true,
analytics: true,
payments: false,
},
};
}
}
// config.module.ts
@Module({
providers: [
{
provide: 'APP_CONFIG',
useFactory: async () => {
const loader = new ConfigLoader();
const config = await loader.loadFromAPI(process.env.CONFIG_API_URL);
return config;
},
},
],
exports: ['APP_CONFIG'],
})
export class ConfigModule {}
3. Cache Initialization (Redis)
// redis.cache.ts
export class RedisCache {
private client: any;
async connect(url: string): Promise<void> {
console.log(`Connecting to Redis: ${url}`);
// Simulate Redis connection
await new Promise(resolve => setTimeout(resolve, 500));
this.client = {
get: async (key: string) => null,
set: async (key: string, value: any) => 'OK',
del: async (key: string) => 1,
flushAll: async () => 'OK',
};
console.log('Redis connected successfully');
}
async get(key: string): Promise<any> {
return this.client.get(key);
}
async set(key: string, value: any, ttl?: number): Promise<string> {
return this.client.set(key, value);
}
async disconnect(): Promise<void> {
console.log('Disconnecting from Redis');
this.client = null;
}
isConnected(): boolean {
return !!this.client;
}
}
// cache.module.ts
@Module({
providers: [
{
provide: 'CACHE_SERVICE',
useFactory: async () => {
const cache = new RedisCache();
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
await cache.connect(redisUrl);
return cache;
},
},
],
exports: ['CACHE_SERVICE'],
})
export class CacheModule {}
// Sử dụng
@Injectable()
export class CacheService {
constructor(
@Inject('CACHE_SERVICE')
private cache: RedisCache,
) {}
async get(key: string): Promise<any> {
return this.cache.get(key);
}
async set(key: string, value: any): Promise<void> {
await this.cache.set(key, value);
}
}
4. Service Health Check
// health-check.ts
export class HealthChecker {
async checkDatabase(connection: any): Promise<boolean> {
try {
await connection.query('SELECT 1');
return true;
} catch {
return false;
}
}
async checkRedis(cache: any): Promise<boolean> {
try {
await cache.set('health-check', 'ok');
await cache.get('health-check');
return true;
} catch {
return false;
}
}
async runStartupChecks(
database: any,
cache: any,
): Promise<{ database: boolean; cache: boolean }> {
console.log('Running startup health checks...');
const dbHealth = await this.checkDatabase(database);
const cacheHealth = await this.checkRedis(cache);
if (!dbHealth) {
throw new Error('Database health check failed');
}
if (!cacheHealth) {
throw new Error('Cache health check failed');
}
console.log('✓ All health checks passed');
return { database: dbHealth, cache: cacheHealth };
}
}
// app.module.ts
@Module({
imports: [DatabaseModule, CacheModule],
providers: [
{
provide: 'HEALTH_CHECK',
useFactory: async (
@Inject('DATABASE_CONNECTION') database: any,
@Inject('CACHE_SERVICE') cache: any,
) => {
const checker = new HealthChecker();
await checker.runStartupChecks(database, cache);
return checker;
},
inject: ['DATABASE_CONNECTION', 'CACHE_SERVICE'],
},
],
})
export class AppModule {}
5. Migration Runner
// migration.runner.ts
export class MigrationRunner {
async runMigrations(
database: any,
migrationsPath: string,
): Promise<void> {
console.log(`Running migrations from ${migrationsPath}...`);
const migrations = [
'create_users_table',
'create_posts_table',
'create_comments_table',
];
for (const migration of migrations) {
console.log(`Running migration: ${migration}`);
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`✓ ${migration} completed`);
}
console.log('All migrations completed');
}
}
// database.module.ts
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
const connection = new DatabaseConnection();
await connection.initialize(configService.get('database'));
// Run migrations
const runner = new MigrationRunner();
await runner.runMigrations(
connection,
configService.get('migrationsPath'),
);
return connection;
},
inject: [ConfigService],
},
],
exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}
6. Environment-Based Initialization
// service.abstract.ts
export abstract class BaseService {
abstract initialize(): Promise<void>;
abstract shutdown(): Promise<void>;
}
// production.service.ts
@Injectable()
export class ProductionService extends BaseService {
async initialize(): Promise<void> {
console.log('Initializing for PRODUCTION');
// Production-specific initialization
await new Promise(resolve => setTimeout(resolve, 2000));
}
async shutdown(): Promise<void> {
console.log('Shutting down PRODUCTION service');
}
}
// development.service.ts
@Injectable()
export class DevelopmentService extends BaseService {
async initialize(): Promise<void> {
console.log('Initializing for DEVELOPMENT');
// Development-specific initialization
await new Promise(resolve => setTimeout(resolve, 500));
}
async shutdown(): Promise<void> {
console.log('Shutting down DEVELOPMENT service');
}
}
// app.module.ts
@Module({
providers: [
{
provide: 'ENV_SERVICE',
useFactory: async () => {
const isProd = process.env.NODE_ENV === 'production';
const service = isProd
? new ProductionService()
: new DevelopmentService();
await service.initialize();
return service;
},
},
],
})
export class AppModule {}
7. Third-Party Library Integration
// elasticsearch.service.ts
export class ElasticsearchService {
private client: any;
async connect(config: {
node: string;
username?: string;
password?: string;
}): Promise<void> {
console.log(`Connecting to Elasticsearch: ${config.node}`);
// Simulate Elasticsearch client initialization
await new Promise(resolve => setTimeout(resolve, 1000));
this.client = {
search: async (query: any) => ({ hits: [] }),
index: async (data: any) => ({ _id: 'doc1' }),
delete: async (id: string) => ({ _id: id }),
close: async () => undefined,
};
console.log('Elasticsearch connected');
}
async search(query: any): Promise<any> {
return this.client.search(query);
}
async index(data: any): Promise<any> {
return this.client.index(data);
}
async disconnect(): Promise<void> {
await this.client.close();
this.client = null;
}
}
// elasticsearch.module.ts
@Module({
providers: [
{
provide: 'ELASTICSEARCH_SERVICE',
useFactory: async (configService: ConfigService) => {
const elasticsearchConfig = configService.get('elasticsearch');
const service = new ElasticsearchService();
await service.connect(elasticsearchConfig);
return service;
},
inject: [ConfigService],
},
],
exports: ['ELASTICSEARCH_SERVICE'],
})
export class ElasticsearchModule {}
8. Async Initialization with Cleanup
// resource.manager.ts
export class ResourceManager {
private resources: Map<string, any> = new Map();
async allocate(name: string, allocator: () => Promise<any>): Promise<void> {
console.log(`Allocating resource: ${name}`);
const resource = await allocator();
this.resources.set(name, resource);
console.log(`✓ Resource allocated: ${name}`);
}
async releaseAll(): Promise<void> {
console.log('Releasing all resources...');
for (const [name, resource] of this.resources.entries()) {
if (resource?.cleanup) {
await resource.cleanup();
}
this.resources.delete(name);
console.log(`✓ Released: ${name}`);
}
}
get<T>(name: string): T {
return this.resources.get(name);
}
}
// resources.module.ts
@Module({
providers: [
{
provide: 'RESOURCE_MANAGER',
useFactory: async (configService: ConfigService) => {
const manager = new ResourceManager();
// Allocate multiple resources
await manager.allocate('database', async () => {
// Database allocation
return { connected: true };
});
await manager.allocate('cache', async () => {
// Cache allocation
return { ready: true };
});
return manager;
},
inject: [ConfigService],
},
],
exports: ['RESOURCE_MANAGER'],
})
export class ResourcesModule {}
Handling Errors in Async Providers
// error-handling.module.ts
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
try {
console.log('Attempting to connect to database...');
// Simulate potential error
const config = configService.get('database');
if (!config) {
throw new Error('Database config not found');
}
const connection = new DatabaseConnection();
await connection.initialize(config);
return connection;
} catch (error) {
console.error('Failed to initialize database:', error.message);
// Option 1: Throw error (application won't start)
throw new Error(`Database initialization failed: ${error.message}`);
// Option 2: Return fallback (graceful degradation)
// return new MockDatabaseConnection();
// Option 3: Log and exit (clean shutdown)
// console.error('Exiting application');
// process.exit(1);
}
},
inject: [ConfigService],
},
],
})
export class DatabaseModule {}
Async Provider Lifecycle
@Injectable()
export class LifecycleService implements OnModuleInit, BeforeApplicationShutdown {
private initialized = false;
async onModuleInit() {
console.log('Module initialized');
// Do async work after provider is created
await this.setupAsync();
this.initialized = true;
}
async beforeApplicationShutdown(signal?: string) {
console.log(`Application shutting down with signal: ${signal}`);
// Cleanup async resources
await this.cleanupAsync();
}
private async setupAsync(): Promise<void> {
console.log('Setting up async resources...');
await new Promise(resolve => setTimeout(resolve, 500));
}
private async cleanupAsync(): Promise<void> {
console.log('Cleaning up async resources...');
await new Promise(resolve => setTimeout(resolve, 500));
}
isInitialized(): boolean {
return this.initialized;
}
}
Best Practices
1. Add Timeouts to Async Operations
// ✅ Đúng - Add timeout
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async () => {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(
() => reject(new Error('Connection timeout')),
10000, // 10 second timeout
),
);
const connectionPromise = createDatabaseConnection();
return Promise.race([connectionPromise, timeoutPromise]);
},
},
],
})
export class DatabaseModule {}
2. Log Initialization Progress
// ✅ Đúng - Log each step
@Module({
providers: [
{
provide: 'SERVICE',
useFactory: async (configService: ConfigService) => {
console.log('[1/5] Loading configuration...');
const config = configService.getConfig();
console.log('[2/5] Validating configuration...');
validateConfig(config);
console.log('[3/5] Establishing connection...');
const connection = await establishConnection(config);
console.log('[4/5] Running migrations...');
await runMigrations(connection);
console.log('[5/5] Service ready');
return connection;
},
inject: [ConfigService],
},
],
})
export class AppModule {}
3. Handle Graceful Shutdown
// ✅ Đúng - Cleanup on shutdown
@Injectable()
export class GracefulShutdownService implements BeforeApplicationShutdown {
constructor(
@Inject('DATABASE_CONNECTION') private database: any,
) {}
async beforeApplicationShutdown(signal?: string) {
console.log(`Received signal: ${signal}`);
console.log('Closing database connections...');
try {
await this.database.close();
console.log('Database connections closed');
} catch (error) {
console.error('Error closing database:', error);
}
}
}
4. Provide Fallback Values
// ✅ Đúng - Provide fallback
@Module({
providers: [
{
provide: 'EXTERNAL_SERVICE',
useFactory: async () => {
try {
return await connectToExternalService();
} catch (error) {
console.warn('Failed to connect to external service, using mock');
return new MockExternalService();
}
},
},
],
})
export class ExternalModule {}
5. Use Environment-Based Configuration
// ✅ Đúng - Environment-aware
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
const env = configService.get('NODE_ENV');
if (env === 'test') {
return new MockDatabaseConnection();
} else if (env === 'production') {
return createProductionConnection();
} else {
return createDevelopmentConnection();
}
},
inject: [ConfigService],
},
],
})
export class DatabaseModule {}
Complete Example
// database.service.ts
@Injectable()
export class DatabaseService {
private connection: any;
async initialize(config: DatabaseConfig): Promise<void> {
console.log('Initializing database...');
try {
await this.connect(config);
await this.verify();
console.log('✓ Database initialized successfully');
} catch (error) {
throw new Error(`Failed to initialize database: ${error.message}`);
}
}
private async connect(config: DatabaseConfig): Promise<void> {
await new Promise(resolve => setTimeout(resolve, 1000));
this.connection = { config };
}
private async verify(): Promise<void> {
await new Promise(resolve => setTimeout(resolve, 500));
}
async query(sql: string): Promise<any> {
return { rows: [] };
}
async close(): Promise<void> {
this.connection = null;
}
}
// database.module.ts
@Module({
providers: [
{
provide: 'DATABASE_SERVICE',
useFactory: async (configService: ConfigService) => {
const config = configService.getDatabaseConfig();
const database = new DatabaseService();
await database.initialize(config);
return database;
},
inject: [ConfigService],
},
],
exports: ['DATABASE_SERVICE'],
})
export class DatabaseModule {}
// app.module.ts
@Module({
imports: [DatabaseModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Kết Luận
Asynchronous Providers là công cụ quan trọng để:
- Khởi tạo resources async trước khi ứng dụng chạy
- Kết nối database một cách an toàn
- Load configuration từ external sources
- Chạy migrations tự động
- Ensure dependencies ready trước khi xử lý requests
Sử dụng Async Providers đúng cách giúp bạn:
- Xây dựng ứng dụng robust và reliable
- Handle complex initialization logic
- Ensure proper resource management
- Implement graceful shutdown
- Support environment-specific initialization
Async Providers là nền tảng cho các production-grade NestJS applications!