Custom Providers trong NestJS
Custom Providers cho phép bạn kiểm soát chính xác cách NestJS tạo và quản lý instances. Thay vì sử dụng decorator @Injectable() trực tiếp, bạn định nghĩa một object cấu hình chi tiết cho providers, cho phép bạn tạo complex initialization logic, substitute implementations, và implement advanced patterns.
Khái Niệm Custom Provider
Custom Provider là một object với cấu trúc định nghĩa:
{
provide: string | symbol | Type<any>, // Token để identify provider
useClass?: Type<any>, // Class để instantiate
useValue?: any, // Static value
useFactory?: (...args) => any, // Factory function
useExisting?: any, // Reference to existing provider
inject?: (string | symbol | Type<any>)[], // Dependencies
}
Các Loại Custom Providers
1. useClass - Class-Based Provider
Sử dụng class khác để tạo instance thay vì class được provide:
// services/database.service.ts
export class DatabaseService {
connect(): void {
console.log('Connecting to database...');
}
}
// services/database-dev.service.ts
export class DatabaseDevService extends DatabaseService {
connect(): void {
console.log('Connecting to DEV database...');
}
}
// services/database-prod.service.ts
export class DatabaseProdService extends DatabaseService {
connect(): void {
console.log('Connecting to PROD database...');
}
}
// database.module.ts
@Module({
providers: [
{
provide: DatabaseService,
useClass:
process.env.NODE_ENV === 'production'
? DatabaseProdService
: DatabaseDevService,
},
],
exports: [DatabaseService],
})
export class DatabaseModule {}
// Sử dụng - inject DatabaseService sẽ nhận dev hoặc prod version
@Injectable()
export class AppService {
constructor(private database: DatabaseService) {}
initialize() {
this.database.connect(); // Calls dev or prod version
}
}
2. useValue - Static Value Provider
Cung cấp một giá trị tĩnh:
// config.ts
export const appConfig = {
appName: 'MyApp',
version: '1.0.0',
environment: 'production',
port: 3000,
};
// app.module.ts
@Module({
providers: [
{
provide: 'APP_CONFIG',
useValue: appConfig,
},
{
provide: 'DATABASE_URL',
useValue: process.env.DATABASE_URL || 'postgresql://localhost/mydb',
},
{
provide: 'FEATURE_FLAGS',
useValue: {
enableNewUI: true,
enableAnalytics: false,
enablePayments: true,
},
},
],
})
export class AppModule {}
// Sử dụng
@Injectable()
export class FeatureService {
constructor(
@Inject('FEATURE_FLAGS') private flags: Record<string, boolean>,
) {}
isFeatureEnabled(featureName: string): boolean {
return this.flags[featureName] || false;
}
}
3. useFactory - Factory Function Provider
Tạo instance thông qua m ột factory function:
// providers/logger.factory.ts
export function createLogger(isDev: boolean) {
return {
log: (message: string) => {
if (isDev) {
console.log(`[DEV LOG] ${message}`);
} else {
console.log(`[${new Date().toISOString()}] ${message}`);
}
},
error: (message: string) => console.error(message),
warn: (message: string) => console.warn(message),
};
}
// app.module.ts
@Module({
providers: [
{
provide: 'LOGGER',
useFactory: (configService: ConfigService) => {
const isDev = configService.get('NODE_ENV') === 'development';
return createLogger(isDev);
},
inject: [ConfigService],
},
],
})
export class AppModule {}
// Sử dụng
@Injectable()
export class UsersService {
constructor(@Inject('LOGGER') private logger: any) {}
getUsers() {
this.logger.log('Fetching users');
return [];
}
}
4. useExisting - Alias Provider
Tạo alias cho existing provider:
// services/logger.service.ts
export class LoggerService {
log(message: string) {
console.log(message);
}
}
// app.module.ts
@Module({
providers: [
LoggerService,
{
provide: 'LOGGER',
useExisting: LoggerService, // Alias
},
{
provide: 'LOGGING_SERVICE',
useExisting: LoggerService, // Another alias
},
],
})
export class AppModule {}
// Sử dụng - Tất cả inject cùng instance
@Controller('users')
export class UsersController {
constructor(
private loggerDirect: LoggerService,
@Inject('LOGGER') private loggerAlias1: LoggerService,
@Inject('LOGGING_SERVICE') private loggerAlias2: LoggerService,
) {}
getUsers() {
// Tất cả là cùng instance
console.log(
this.loggerDirect === this.loggerAlias1 &&
this.loggerAlias1 === this.loggerAlias2
); // true
}
}
Ví Dụ Custom Providers Thực Tế
1. Strategy Pattern with useClass
// interfaces/payment-strategy.ts
export interface PaymentStrategy {
pay(amount: number): Promise<void>;
}
// strategies/stripe.strategy.ts
@Injectable()
export class StripePaymentStrategy implements PaymentStrategy {
async pay(amount: number): Promise<void> {
console.log(`Processing payment of $${amount} via Stripe`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// strategies/paypal.strategy.ts
@Injectable()
export class PayPalPaymentStrategy implements PaymentStrategy {
async pay(amount: number): Promise<void> {
console.log(`Processing payment of $${amount} via PayPal`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// payment.module.ts
@Module({
providers: [
{
provide: 'PAYMENT_STRATEGY',
useClass:
process.env.PAYMENT_PROVIDER === 'paypal'
? PayPalPaymentStrategy
: StripePaymentStrategy,
},
],
exports: ['PAYMENT_STRATEGY'],
})
export class PaymentModule {}
// Sử dụng
@Injectable()
export class OrderService {
constructor(
@Inject('PAYMENT_STRATEGY') private paymentStrategy: PaymentStrategy,
) {}
async createOrder(amount: number) {
await this.paymentStrategy.pay(amount);
return { orderId: '123', amount, paid: true };
}
}
2. Configuration-Based Provider Selection
// enums/logger-type.enum.ts
export enum LoggerType {
CONSOLE = 'console',
FILE = 'file',
CLOUD = 'cloud',
}
// loggers/console.logger.ts
@Injectable()
export class ConsoleLogger {
log(message: string) {
console.log(`[CONSOLE] ${message}`);
}
}
// loggers/file.logger.ts
@Injectable()
export class FileLogger {
log(message: string) {
console.log(`[FILE] ${message}`); // In practice, write to file
}
}
// loggers/cloud.logger.ts
@Injectable()
export class CloudLogger {
log(message: string) {
console.log(`[CLOUD] ${message}`); // Send to cloud logging service
}
}
// logging.module.ts
@Module({
providers: [
{
provide: 'LOGGER',
useClass: getLoggerClass(process.env.LOGGER_TYPE),
},
],
exports: ['LOGGER'],
})
export class LoggingModule {}
function getLoggerClass(type: string) {
switch (type) {
case LoggerType.CONSOLE:
return ConsoleLogger;
case LoggerType.FILE:
return FileLogger;
case LoggerType.CLOUD:
return CloudLogger;
default:
return ConsoleLogger;
}
}
3. Factory with Dependency Injection
// interfaces/database.interface.ts
export interface IDatabase {
query(sql: string): Promise<any>;
close(): Promise<void>;
}
// database/postgres.database.ts
export class PostgresDatabase implements IDatabase {
constructor(private config: DatabaseConfig) {}
async query(sql: string): Promise<any> {
console.log(`Postgres: ${sql}`);
return [];
}
async close(): Promise<void> {
console.log('Postgres closed');
}
}
// database/mongodb.database.ts
export class MongoDBDatabase implements IDatabase {
constructor(private config: DatabaseConfig) {}
async query(sql: string): Promise<any> {
console.log(`MongoDB: ${sql}`);
return [];
}
async close(): Promise<void> {
console.log('MongoDB closed');
}
}
// database/database.factory.ts
export function createDatabase(
config: DatabaseConfig,
): IDatabase {
if (config.type === 'postgres') {
return new PostgresDatabase(config);
} else if (config.type === 'mongodb') {
return new MongoDBDatabase(config);
}
throw new Error(`Unknown database type: ${config.type}`);
}
// database.module.ts
@Module({
providers: [
{
provide: 'DATABASE',
useFactory: (configService: ConfigService) => {
const config = configService.getDatabaseConfig();
return createDatabase(config);
},
inject: [ConfigService],
},
],
exports: ['DATABASE'],
})
export class DatabaseModule {}
4. Mock Provider for Testing
// services/api.service.ts
@Injectable()
export class ApiService {
async fetchData(url: string): Promise<any> {
// Real HTTP call
const response = await fetch(url);
return response.json();
}
}
// mocks/api.mock.ts
export class MockApiService {
async fetchData(url: string): Promise<any> {
// Mocked data
return {
id: 1,
name: 'Mocked Data',
description: 'This is mocked',
};
}
}
// app.module.ts
@Module({
providers: [
{
provide: ApiService,
useClass:
process.env.NODE_ENV === 'test'
? MockApiService
: ApiService,
},
],
})
export class AppModule {}
5. Multi-Valued Provider (Array)
// plugins/plugin.interface.ts
export interface Plugin {
name: string;
execute(): void;
}
// plugins/analytics.plugin.ts
@Injectable()
export class AnalyticsPlugin implements Plugin {
name = 'Analytics';
execute() {
console.log('Running analytics plugin');
}
}
// plugins/notification.plugin.ts
@Injectable()
export class NotificationPlugin implements Plugin {
name = 'Notification';
execute() {
console.log('Running notification plugin');
}
}
// plugins.module.ts
@Module({
providers: [
AnalyticsPlugin,
NotificationPlugin,
{
provide: 'PLUGINS',
useValue: [
// Can include instances or tokens
],
inject: [AnalyticsPlugin, NotificationPlugin],
},
],
})
export class PluginsModule {}
6. Provider with Custom Scope
// database-session.ts
@Injectable({ scope: Scope.REQUEST })
export class DatabaseSession {
private sessionId = Math.random().toString();
getSessionId() {
return this.sessionId;
}
}
// app.module.ts
@Module({
providers: [
{
provide: 'DB_SESSION',
useClass: DatabaseSession,
scope: Scope.REQUEST, // Custom scope
},
],
})
export class AppModule {}
7. Non-Class Provider with Methods
// logger.provider.ts
export const loggerProvider = {
provide: 'LOGGER',
useValue: {
debug: (message: string) => console.debug(message),
info: (message: string) => console.info(message),
warn: (message: string) => console.warn(message),
error: (message: string) => console.error(message),
},
};
// app.module.ts
@Module({
providers: [loggerProvider],
})
export class AppModule {}
// Sử dụng
@Injectable()
export class MyService {
constructor(@Inject('LOGGER') private logger: any) {}
execute() {
this.logger.info('Executing service');
}
}
8. Computed Provider Values
// app.module.ts
@Module({
providers: [
{
provide: 'DATABASE_CONFIG',
useValue: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
username: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'mydb',
},
},
{
provide: 'DATABASE_URL',
useFactory: (config: any) => {
return `postgresql://${config.username}:${config.password}@${config.host}:${config.port}/${config.database}`;
},
inject: ['DATABASE_CONFIG'],
},
],
})
export class AppModule {}
9. Lazy Provider Instantiation
// lazy-service.ts
export class LazyService {
constructor() {
console.log('LazyService instantiated');
}
execute() {
return 'executed';
}
}
// app.module.ts
@Module({
providers: [
{
provide: 'LAZY_SERVICE',
useFactory: () => {
return new LazyService(); // Created on first use
},
},
],
})
export class AppModule {}
10. Conditional Provider Registration
// app.module.ts
@Module({
providers: getProviders(),
})
export class AppModule {}
function getProviders() {
const providers = [
ConfigService, // Always included
];
// Conditionally add providers
if (process.env.ENABLE_CACHE === 'true') {
providers.push({
provide: 'CACHE_SERVICE',
useClass: RedisCacheService,
});
}
if (process.env.ENABLE_MONITORING === 'true') {
providers.push({
provide: 'MONITORING',
useClass: MonitoringService,
});
}
return providers;
}
Advanced Custom Provider Patterns
Provider Factory Pattern
// provider.factory.ts
export class ProviderFactory {
static createDatabaseProvider(type: 'postgres' | 'mongodb') {
if (type === 'postgres') {
return {
provide: 'DATABASE',
useClass: PostgresDatabase,
};
}
return {
provide: 'DATABASE',
useClass: MongoDBDatabase,
};
}
static createLoggerProvider(env: 'dev' | 'prod') {
return {
provide: 'LOGGER',
useFactory: () => createLogger(env === 'dev'),
};
}
}
// app.module.ts
@Module({
providers: [
ProviderFactory.createDatabaseProvider('postgres'),
ProviderFactory.createLoggerProvider('dev'),
],
})
export class AppModule {}
Dynamic Provider Registration
// module.config.ts
export interface ModuleConfig {
providers: any[];
imports?: any[];
exports?: any[];
}
// app.module.ts
@Module(
buildModuleConfig({
enableCache: true,
enableMonitoring: false,
databaseType: 'postgres',
}),
)
export class AppModule {}
function buildModuleConfig(config: any): ModuleConfig {
const providers = [ConfigService];
if (config.enableCache) {
providers.push({
provide: 'CACHE',
useClass: CacheService,
});
}
if (config.enableMonitoring) {
providers.push({
provide: 'MONITORING',
useClass: MonitoringService,
});
}
return { providers };
}
Best Practices
1. Use String Tokens for Non-Classes
// ✅ Đúng
@Module({
providers: [
{
provide: 'DATABASE_URL', // String token
useValue: 'postgresql://...',
},
],
})
export class DatabaseModule {}
// ❌ Sai
@Module({
providers: [
{
provide: DatabaseService, // Class used as string token
useValue: 'postgresql://...',
},
],
})
export class DatabaseModule {}
2. Type Your Injected Values
// ✅ Đúng - Clear types
export interface DatabaseConfig {
host: string;
port: number;
}
@Module({
providers: [
{
provide: 'DB_CONFIG',
useValue: {
host: 'localhost',
port: 5432,
} as DatabaseConfig,
},
],
})
export class DatabaseModule {}
// Sử dụng
@Injectable()
export class UserService {
constructor(
@Inject('DB_CONFIG') private config: DatabaseConfig,
) {}
}
3. Use Symbols for Private Tokens
// ✅ Đúng - Symbols prevent naming conflicts
export const PRIVATE_LOGGER = Symbol('PrivateLogger');
@Module({
providers: [
{
provide: PRIVATE_LOGGER,
useClass: LoggerService,
},
],
})
export class AppModule {}
4. Document Custom Providers
// ✅ Đúng - Clear documentation
/**
* Provides database connection instance
* - Type: Dynamic based on environment
* - Scope: Application singleton
* - Fallback: None - will fail if database unavailable
*/
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useClass: process.env.DATABASE_TYPE === 'mongodb'
? MongoDBConnection
: PostgresConnection,
},
],
})
export class DatabaseModule {}
5. Create Reusable Provider Builders
// ✅ Đúng - Reusable provider builders
export function createStrategyProvider<T>(
token: string,
strategyMap: Record<string, any>,
defaultKey: string,
) {
return {
provide: token,
useFactory: (configService: ConfigService) => {
const selected = configService.get('STRATEGY') || defaultKey;
const Strategy = strategyMap[selected];
if (!Strategy) {
throw new Error(`Unknown strategy: ${selected}`);
}
return new Strategy();
},
inject: [ConfigService],
};
}
// Sử dụng
@Module({
providers: [
createStrategyProvider('PAYMENT_STRATEGY', {
stripe: StripeStrategy,
paypal: PayPalStrategy,
}, 'stripe'),
],
})
export class PaymentModule {}
Complete Example
// config/app.config.ts
export interface AppConfig {
isDevelopment: boolean;
isProd: boolean;
logger: 'console' | 'file';
database: 'postgres' | 'mongodb';
cache: 'redis' | 'memory';
}
// providers/config.provider.ts
export const configProvider = {
provide: 'APP_CONFIG',
useValue: {
isDevelopment: process.env.NODE_ENV === 'development',
isProd: process.env.NODE_ENV === 'production',
logger: process.env.LOGGER_TYPE || 'console',
database: process.env.DATABASE_TYPE || 'postgres',
cache: process.env.CACHE_TYPE || 'memory',
} as AppConfig,
};
// providers/logger.provider.ts
export const loggerProvider = {
provide: 'LOGGER',
useFactory: (config: AppConfig) => {
if (config.logger === 'console') {
return {
log: (msg: string) => console.log(`[LOG] ${msg}`),
error: (msg: string) => console.error(`[ERROR] ${msg}`),
};
}
return {
log: (msg: string) => console.log(`[FILE] ${msg}`),
error: (msg: string) => console.error(`[FILE] ${msg}`),
};
},
inject: ['APP_CONFIG'],
};
// providers/database.provider.ts
export const databaseProvider = {
provide: 'DATABASE',
useFactory: (config: AppConfig) => {
const DatabaseClass = config.database === 'postgres'
? PostgresDatabase
: MongoDBDatabase;
return new DatabaseClass(config);
},
inject: ['APP_CONFIG'],
};
// app.module.ts
@Module({
providers: [
configProvider,
loggerProvider,
databaseProvider,
],
})
export class AppModule {}
// Sử dụng
@Injectable()
export class AppService {
constructor(
@Inject('LOGGER') private logger: any,
@Inject('DATABASE') private database: any,
) {}
execute() {
this.logger.log('Application started');
// Use database
}
}
Kết Luận
Custom Providers là công cụ mạnh mẽ để:
- Kiểm soát tính flexible của providers
- Implement advanced patterns như Strategy, Factory
- Support multiple implementations dựa trên configuration
- Create reusable provider logic
- Dynamically select implementations tại runtime
Sử dụng Custom Providers đúng cách giúp bạn:
- Xây dựng highly configurable applications
- Implement complex initialization logic
- Support multiple environments (dev, prod, test)
- Create library-grade modules
- Implement design patterns efficiently
Custom Providers là nền tảng cho enterprise-grade NestJS applications!