Dynamic Modules trong NestJS
Dynamic Modules là một pattern mạnh mẽ cho phép bạn tạo modules có configuration động. Thay vì tạo static modules, dynamic modules cho phép bạn pass configuration khi import module, làm cho modules reusable và flexible hơn.
Khái Niệm Dynamic Module
Dynamic Module là một module trả về một DynamicModule object thay vì một class. Nó cho phép:
- Pass configuration khi import module
- Dynamically create providers dựa trên configuration
- Reuse modules với các config khác nhau
- Control module behavior từ parent module
// Static Module (không flexible)
@Module({
providers: [DatabaseService],
})
export class DatabaseModule {}
// Dynamic Module (flexible!)
export class DatabaseModule {
static forRoot(options: DatabaseOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: 'DATABASE_OPTIONS',
useValue: options,
},
DatabaseService,
],
exports: [DatabaseService],
};
}
}
DynamicModule Interface
export interface DynamicModule {
module: Type<any>;
imports?: Array<Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
controllers?: Type<any>[];
providers?: Provider[];
exports?: Array<DynamicModule | string | symbol | Type<any> | Promise<DynamicModule | string | symbol | Type<any>>>;
global?: boolean;
}
Cơ Bản về Dynamic Modules
1. Simple Dynamic Module with forRoot()
// config/database.config.ts
export interface DatabaseConfig {
host: string;
port: number;
username: string;
password: string;
database: string;
}
// services/database.service.ts
@Injectable()
export class DatabaseService {
constructor(@Inject('DATABASE_CONFIG') private config: DatabaseConfig) {}
connect() {
console.log(`Connecting to ${this.config.host}:${this.config.port}`);
return { connected: true };
}
getConnection() {
return { database: this.config.database };
}
}
// database.module.ts
@Module({})
export class DatabaseModule {
static forRoot(config: DatabaseConfig): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: 'DATABASE_CONFIG',
useValue: config,
},
DatabaseService,
],
exports: [DatabaseService],
};
}
}
// app.module.ts
@Module({
imports: [
DatabaseModule.forRoot({
host: 'localhost',
port: 5432,
username: 'user',
password: 'pass',
database: 'mydb',
}),
],
})
export class AppModule {}
2. Dynamic Module with forFeature()
// repository/user.repository.ts
@Injectable()
export class UserRepository {
constructor(
@Inject('DATABASE_CONFIG') private config: DatabaseConfig,
) {}
async findAll() {
console.log(`Fetching users from ${this.config.database}`);
return [];
}
}
// users.module.ts
@Module({})
export class UsersModule {
static forRoot(config: DatabaseConfig): DynamicModule {
return {
module: UsersModule,
imports: [DatabaseModule.forRoot(config)],
providers: [UsersService, UserRepository],
controllers: [UsersController],
exports: [UsersService],
};
}
static forFeature(): DynamicModule {
return {
module: UsersModule,
providers: [UsersService, UserRepository],
controllers: [UsersController],
exports: [UsersService],
};
}
}
// app.module.ts
@Module({
imports: [
DatabaseModule.forRoot(dbConfig),
UsersModule.forFeature(),
ProductsModule.forFeature(),
],
})
export class AppModule {}
Ví Dụ Dynamic Modules Thực Tế
1. Configuration Module
// config/app.config.ts
export interface AppConfig {
nodeEnv: 'development' | 'production' | 'test';
port: number;
apiPrefix: string;
logLevel: 'error' | 'warn' | 'info' | 'debug';
jwt: {
secret: string;
expiresIn: string;
};
}
// config.module.ts
@Module({})
export class ConfigModule {
static forRoot(config: AppConfig): DynamicModule {
// Validate configuration
if (!config.jwt.secret) {
throw new Error('JWT secret must be provided');
}
return {
module: ConfigModule,
global: true, // Global module
providers: [
{
provide: 'APP_CONFIG',
useValue: config,
},
{
provide: 'CONFIG_SERVICE',
useFactory: (appConfig: AppConfig) => {
return {
get: (key: string) => {
return appConfig[key];
},
getAll: () => appConfig,
};
},
inject: ['APP_CONFIG'],
},
],
exports: ['APP_CONFIG', 'CONFIG_SERVICE'],
};
}
}
// Sử dụng
@Module({
imports: [
ConfigModule.forRoot({
nodeEnv: 'production',
port: 3000,
apiPrefix: 'api/v1',
logLevel: 'info',
jwt: {
secret: 'my-secret-key',
expiresIn: '24h',
},
}),
],
})
export class AppModule {}
2. Logger Module
// logger/logger.config.ts
export interface LoggerConfig {
level: 'debug' | 'info' | 'warn' | 'error';
format: 'json' | 'text';
includeTimestamp: boolean;
}
// logger/logger.service.ts
@Injectable()
export class LoggerService {
constructor(@Inject('LOGGER_CONFIG') private config: LoggerConfig) {}
debug(message: string, context?: string) {
if (this.config.level === 'debug') {
console.log(`[DEBUG] ${context || ''}: ${message}`);
}
}
info(message: string, context?: string) {
if (['debug', 'info'].includes(this.config.level)) {
console.log(`[INFO] ${context || ''}: ${message}`);
}
}
warn(message: string, context?: string) {
if (['debug', 'info', 'warn'].includes(this.config.level)) {
console.warn(`[WARN] ${context || ''}: ${message}`);
}
}
error(message: string, error?: any, context?: string) {
console.error(`[ERROR] ${context || ''}: ${message}`, error);
}
}
// logger.module.ts
@Module({})
export class LoggerModule {
static forRoot(config: LoggerConfig): DynamicModule {
return {
module: LoggerModule,
global: true,
providers: [
{
provide: 'LOGGER_CONFIG',
useValue: config,
},
LoggerService,
],
exports: [LoggerService],
};
}
}
3. Cache Module
// cache/cache.config.ts
export interface CacheConfig {
ttl: number; // Time to live in seconds
maxSize: number; // Maximum cache size
driver: 'memory' | 'redis';
redisUrl?: string;
}
// cache/cache.service.ts
@Injectable()
export class CacheService {
private cache: Map<string, { value: any; expiresAt: number }> = new Map();
constructor(@Inject('CACHE_CONFIG') private config: CacheConfig) {}
set(key: string, value: any, ttl?: number) {
const expiresAt = Date.now() + ((ttl || this.config.ttl) * 1000);
if (this.cache.size >= this.config.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, { value, expiresAt });
}
get(key: string) {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return null;
}
return entry.value;
}
delete(key: string) {
this.cache.delete(key);
}
clear() {
this.cache.clear();
}
}
// cache.module.ts
@Module({})
export class CacheModule {
static forRoot(config: CacheConfig): DynamicModule {
const providers = [
{
provide: 'CACHE_CONFIG',
useValue: config,
},
];
// Dynamically create cache service based on driver
if (config.driver === 'memory') {
providers.push(CacheService);
} else if (config.driver === 'redis') {
// Redis implementation
providers.push({
provide: 'CACHE_SERVICE',
useFactory: async () => {
// Connect to Redis
return new RedisCacheService(config.redisUrl);
},
});
}
return {
module: CacheModule,
global: true,
providers,
exports: [CacheService, 'CACHE_SERVICE'],
};
}
}
4. Database ORM Module
// typeorm.config.ts
export interface TypeOrmModuleOptions {
type: 'postgres' | 'mysql' | 'sqlite' | 'mongodb';
host: string;
port: number;
username: string;
password: string;
database: string;
entities: any[];
synchronize: boolean;
}
// typeorm.module.ts
@Module({})
export class TypeOrmModule {
static forRoot(options: TypeOrmModuleOptions): DynamicModule {
return {
module: TypeOrmModule,
global: true,
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async () => {
// Initialize database connection
const connection = await createConnection({
...options,
type: options.type,
});
return connection;
},
},
{
provide: 'TYPEORM_OPTIONS',
useValue: options,
},
],
exports: ['DATABASE_CONNECTION', 'TYPEORM_OPTIONS'],
};
}
static forFeature(entities: any[]): DynamicModule {
return {
module: TypeOrmModule,
providers: [
{
provide: 'ENTITY_METADATA',
useValue: entities,
},
],
exports: ['ENTITY_METADATA'],
};
}
}
5. JWT Module
// jwt.config.ts
export interface JwtConfig {
secret: string;
expiresIn: string;
algorithm: 'HS256' | 'RS256';
}
// jwt.service.ts
@Injectable()
export class JwtService {
constructor(@Inject('JWT_CONFIG') private config: JwtConfig) {}
sign(payload: any) {
return jwt.sign(payload, this.config.secret, {
algorithm: this.config.algorithm as any,
expiresIn: this.config.expiresIn,
});
}
verify(token: string) {
return jwt.verify(token, this.config.secret, {
algorithms: [this.config.algorithm as any],
});
}
decode(token: string) {
return jwt.decode(token);
}
}
// jwt.module.ts
@Module({})
export class JwtModule {
static forRoot(config: JwtConfig): DynamicModule {
// Validate configuration
if (!config.secret) {
throw new Error('JWT secret is required');
}
return {
module: JwtModule,
global: true,
providers: [
{
provide: 'JWT_CONFIG',
useValue: config,
},
JwtService,
],
exports: [JwtService],
};
}
static forFeature(): DynamicModule {
return {
module: JwtModule,
providers: [JwtService],
exports: [JwtService],
};
}
}
6. Email/Notification Module
// mailer.config.ts
export interface MailerConfig {
host: string;
port: number;
secure: boolean;
auth: {
user: string;
password: string;
};
from: string;
}
// mailer.service.ts
@Injectable()
export class MailerService {
private transporter: any;
constructor(@Inject('MAILER_CONFIG') private config: MailerConfig) {
this.initializeTransporter();
}
private initializeTransporter() {
// Initialize email transporter (e.g., nodemailer)
this.transporter = {
sendMail: async (options: any) => {
console.log(`Sending email to ${options.to}`);
return { success: true };
},
};
}
async sendEmail(to: string, subject: string, html: string) {
return this.transporter.sendMail({
from: this.config.from,
to,
subject,
html,
});
}
async sendWelcomeEmail(email: string, name: string) {
return this.sendEmail(
email,
'Welcome!',
`<h1>Welcome ${name}!</h1>`,
);
}
}
// mailer.module.ts
@Module({})
export class MailerModule {
static forRoot(config: MailerConfig): DynamicModule {
return {
module: MailerModule,
global: true,
providers: [
{
provide: 'MAILER_CONFIG',
useValue: config,
},
MailerService,
],
exports: [MailerService],
};
}
}
7. Plugin Module with Configuration
// plugin.interface.ts
export interface Plugin {
name: string;
execute(): void;
}
export interface PluginConfig {
plugins: Plugin[];
autoLoad: boolean;
timeout: number;
}
// plugin.service.ts
@Injectable()
export class PluginService {
constructor(@Inject('PLUGIN_CONFIG') private config: PluginConfig) {}
loadPlugin(plugin: Plugin) {
console.log(`Loading plugin: ${plugin.name}`);
if (this.config.autoLoad) {
plugin.execute();
}
}
loadAllPlugins() {
this.config.plugins.forEach((plugin) => this.loadPlugin(plugin));
}
getLoadedPlugins() {
return this.config.plugins;
}
}
// plugin.module.ts
@Module({})
export class PluginModule {
static forRoot(config: PluginConfig): DynamicModule {
return {
module: PluginModule,
global: true,
providers: [
{
provide: 'PLUGIN_CONFIG',
useValue: config,
},
PluginService,
],
exports: [PluginService],
};
}
}
8. Async Dynamic Module with Factory
// async-config.module.ts
export interface AsyncModuleOptions {
isGlobal?: boolean;
useFactory?: (...args: any[]) => any | Promise<any>;
inject?: any[];
}
@Module({})
export class AsyncConfigModule {
static forRootAsync(options: AsyncModuleOptions): DynamicModule {
const asyncProviders = [
{
provide: 'ASYNC_CONFIG',
useFactory: options.useFactory,
inject: options.inject || [],
},
];
return {
module: AsyncConfigModule,
global: options.isGlobal ?? false,
providers: asyncProviders,
exports: ['ASYNC_CONFIG'],
};
}
}
// app.module.ts
@Module({
imports: [
AsyncConfigModule.forRootAsync({
isGlobal: true,
useFactory: async (configService: ConfigService) => {
const config = await configService.loadConfig();
return config;
},
inject: [ConfigService],
}),
],
})
export class AppModule {}
9. Validation in Dynamic Module
// validated-module.ts
@Module({})
export class ValidatedModule {
static forRoot(options: any): DynamicModule {
// Validate options
const schema = Joi.object({
host: Joi.string().required(),
port: Joi.number().required(),
username: Joi.string().required(),
password: Joi.string().required(),
});
const { error, value } = schema.validate(options);
if (error) {
throw new Error(`Invalid configuration: ${error.message}`);
}
return {
module: ValidatedModule,
providers: [
{
provide: 'VALIDATED_CONFIG',
useValue: value,
},
],
exports: ['VALIDATED_CONFIG'],
};
}
}
10. Environment-Based Dynamic Module
// env-based.module.ts
@Module({})
export class EnvBasedModule {
static forRoot(): DynamicModule {
const nodeEnv = process.env.NODE_ENV || 'development';
const isDevelopment = nodeEnv === 'development';
const providers = [
{
provide: 'ENV_CONFIG',
useValue: {
isDevelopment,
isDev: isDevelopment,
isProd: !isDevelopment,
env: nodeEnv,
},
},
];
// Conditionally add debug provider only in development
if (isDevelopment) {
providers.push({
provide: 'DEBUG_SERVICE',
useValue: {
enableDebug: () => console.log('Debug enabled'),
},
});
}
return {
module: EnvBasedModule,
global: true,
providers,
exports: ['ENV_CONFIG', ...(isDevelopment ? ['DEBUG_SERVICE'] : [])],
};
}
}
Dynamic Module Patterns
forRoot() Pattern
@Module({})
export class MyModule {
// Root configuration - setup once
static forRoot(options: MyOptions): DynamicModule {
return {
module: MyModule,
global: true,
providers: [
{
provide: 'MY_OPTIONS',
useValue: options,
},
MyService,
],
exports: [MyService],
};
}
}
// Sử dụng
@Module({
imports: [MyModule.forRoot({ /* options */ })],
})
export class AppModule {}
forFeature() Pattern
@Module({})
export class MyModule {
// Feature configuration - can be used multiple times
static forFeature(features: string[]): DynamicModule {
return {
module: MyModule,
providers: [
{
provide: 'MY_FEATURES',
useValue: features,
},
FeatureService,
],
exports: [FeatureService],
};
}
}
// Sử dụng
@Module({
imports: [
MyModule.forFeature(['feature1', 'feature2']),
MyModule.forFeature(['feature3']),
],
})
export class FeatureModule {}
forRootAsync() Pattern
export interface ModuleAsyncOptions {
useFactory?: (...args: any[]) => Promise<any> | any;
inject?: any[];
}
@Module({})
export class MyModule {
static forRootAsync(options: ModuleAsyncOptions): DynamicModule {
return {
module: MyModule,
global: true,
providers: [
{
provide: 'MY_OPTIONS',
useFactory: options.useFactory,
inject: options.inject || [],
},
MyService,
],
exports: [MyService],
};
}
}
// Sử dụng
@Module({
imports: [
MyModule.forRootAsync({
useFactory: async (configService: ConfigService) => {
return await configService.loadConfig();
},
inject: [ConfigService],
}),
],
})
export class AppModule {}
Best Practices
1. Type Safety
// ❌ Sai - No type safety
static forRoot(options: any): DynamicModule {
return { /* ... */ };
}
// ✅ Đúng - Type-safe
export interface MyModuleOptions {
host: string;
port: number;
}
static forRoot(options: MyModuleOptions): DynamicModule {
return { /* ... */ };
}
2. Validate Configuration
// ✅ Đúng - Validate options
static forRoot(options: MyModuleOptions): DynamicModule {
if (!options.host || !options.port) {
throw new Error('Host and port must be provided');
}
return { /* ... */ };
}
3. Use Global Modules Wisely
// ❌ Sai - Everything global
static forRoot(options: Options): DynamicModule {
return {
module: MyModule,
global: true, // Global mọi thứ
providers,
};
}
// ✅ Đúng - Only global if needed
static forRoot(options: Options): DynamicModule {
return {
module: MyModule,
global: true, // Chỉ global nếu là core module
providers,
};
}
4. Provide Clear Documentation
/**
* Register MyModule with configuration
*
* @example
* @Module({
* imports: [MyModule.forRoot({ host: 'localhost' })]
* })
* export class AppModule {}
*/
static forRoot(options: MyModuleOptions): DynamicModule {
return { /* ... */ };
}
5. Support Both Sync and Async Configuration
@Module({})
export class MyModule {
// Synchronous
static forRoot(options: MyOptions): DynamicModule {
return { /* ... */ };
}
// Asynchronous
static forRootAsync(options: AsyncOptions): DynamicModule {
return {
module: MyModule,
providers: [
{
provide: 'MY_OPTIONS',
useFactory: options.useFactory,
inject: options.inject,
},
],
};
}
}
Complete Example
// jwt-auth.module.ts
export interface JwtAuthOptions {
secret: string;
expiresIn: string;
}
@Module({})
export class JwtAuthModule {
static forRoot(options: JwtAuthOptions): DynamicModule {
if (!options.secret) {
throw new Error('JWT secret is required');
}
return {
module: JwtAuthModule,
global: true,
providers: [
{
provide: 'JWT_OPTIONS',
useValue: options,
},
{
provide: 'JWT_SERVICE',
useClass: JwtService,
},
],
exports: ['JWT_SERVICE'],
};
}
static forRootAsync(
options: AsyncModuleOptions,
): DynamicModule {
return {
module: JwtAuthModule,
global: true,
providers: [
{
provide: 'JWT_OPTIONS',
useFactory: options.useFactory,
inject: options.inject || [],
},
{
provide: 'JWT_SERVICE',
useClass: JwtService,
},
],
exports: ['JWT_SERVICE'],
};
}
}
@Injectable()
export class JwtService {
constructor(@Inject('JWT_OPTIONS') private options: JwtAuthOptions) {}
sign(payload: any) {
return jwt.sign(payload, this.options.secret, {
expiresIn: this.options.expiresIn,
});
}
verify(token: string) {
return jwt.verify(token, this.options.secret);
}
}
// app.module.ts
@Module({
imports: [
JwtAuthModule.forRoot({
secret: 'my-secret-key',
expiresIn: '24h',
}),
],
})
export class AppModule {}
// Hoặc async
@Module({
imports: [
JwtAuthModule.forRootAsync({
useFactory: async (configService: ConfigService) => {
return {
secret: await configService.get('JWT_SECRET'),
expiresIn: await configService.get('JWT_EXPIRES_IN'),
};
},
inject: [ConfigService],
}),
],
})
export class AppAsyncModule {}
Kết Luận
Dynamic Modules là công cụ mạnh mẽ để:
- Create reusable modules với configuration khác nhau
- Pass configuration khi import module
- Control module behavior dynamically
- Build library-grade modules
- Implement flexible architectures
Sử dụng Dynamic Modules đúng cách giúp bạn:
- Tạo highly configurable modules
- Build enterprise-grade applications
- Share modules giữa projects
- Implement plugin systems
- Support multiple environments
- Create framework-like abstractions
Dynamic Modules là pattern thiết yếu cho việc xây dựng scalable, flexible, và reusable NestJS applications!