Skip to main content

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!