Module Reference trong NestJS
Module Reference cung cấp cách để dynamically resolve providers tại runtime. Nó cho phép bạn truy cập và inject providers từ modules khác mà không cần khai báo import tĩnh. Điều này hữu ích cho các use cases phức tạp như circular dependencies hoặc lazy loading.
Khái Niệm Module Reference
Module Reference là một service cho phép bạn:
- Dynamically resolve providers tại runtime
- Truy cập providers từ bất kỳ module nào
- Tránh circular dependencies
- Lazy load modules và providers
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
@Injectable()
export class AppService {
constructor(private moduleRef: ModuleRef) {}
// Sử dụng ModuleRef để lấy provider
getProvider() {
const service = this.moduleRef.get(SomeService);
return service;
}
}
ModuleRef API
get() - Lấy Provider
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
@Injectable()
export class AppService {
constructor(private moduleRef: ModuleRef) {}
// Lấy provider theo class
getService() {
const service = this.moduleRef.get(UsersService);
return service;
}
// Lấy provider theo string token
getByToken() {
const service = this.moduleRef.get('USERS_SERVICE');
return service;
}
// Lấy provider với strict flag (throw error nếu không tìm thấy)
getStrict() {
try {
const service = this.moduleRef.get(UsersService, { strict: true });
return service;
} catch (error) {
console.error('Service not found', error);
}
}
// Lấy provider với non-strict mode (return undefined nếu không tìm thấy)
getNonStrict() {
const service = this.moduleRef.get(UsersService, { strict: false });
return service; // undefined nếu không tìm thấy
}
}
resolve() - Resolve Provider Asynchronously
@Injectable()
export class AppService {
constructor(private moduleRef: ModuleRef) {}
// Resolve provider một cách async
async resolveService() {
const service = await this.moduleRef.resolve(UsersService);
return service;
}
// Resolve provider từ module cụ thể
async resolveFromModule() {
const service = await this.moduleRef.resolve(
UsersService,
UsersModule, // Resolve từ module này
);
return service;
}
// Resolve provider asynchronously cùng dependencies
async resolveAsync() {
const serviceA = await this.moduleRef.resolve(ServiceA);
const serviceB = await this.moduleRef.resolve(ServiceB);
return { serviceA, serviceB };
}
}
create() - Tạo Dynamic Instance
@Injectable()
export class AppService {
constructor(private moduleRef: ModuleRef) {}
// Tạo instance mới của provider
createInstance() {
const service = this.moduleRef.create(UsersService);
return service; // Instance mới, không được cache
}
// Useful cho REQUEST-scoped hoặc TRANSIENT providers
async createTransient() {
const instance = await this.moduleRef.create(RequestScopedService);
return instance;
}
}
Ví Dụ Module Reference Thực Tế
1. Accessing Providers Dynamically
// users.service.ts
@Injectable()
export class UsersService {
getUsers() {
return ['Alice', 'Bob', 'Charlie'];
}
}
// products.service.ts
@Injectable()
export class ProductsService {
getProducts() {
return ['Product A', 'Product B'];
}
}
// app.service.ts
@Injectable()
export class AppService {
constructor(private moduleRef: ModuleRef) {}
// Dynamically lấy bất kỳ service nào
getServiceData(serviceName: string) {
let service;
switch (serviceName) {
case 'users':
service = this.moduleRef.get(UsersService);
return service.getUsers();
case 'products':
service = this.moduleRef.get(ProductsService);
return service.getProducts();
default:
return null;
}
}
// Hoặc sử dụng map
private serviceMap = {
users: UsersService,
products: ProductsService,
};
getService(key: string) {
const ServiceClass = this.serviceMap[key];
if (!ServiceClass) {
throw new NotFoundException(`Service ${key} not found`);
}
return this.moduleRef.get(ServiceClass);
}
}
2. Resolving Circular Dependencies
// auth.service.ts
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService, // Circular dependency!
) {}
async validateUser(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
return user;
}
}
// users.service.ts
@Injectable()
export class UsersService {
constructor(
private authService: AuthService, // Circular dependency!
) {}
async findByEmail(email: string) {
return { id: 1, email };
}
}
// ❌ Sai - Circular dependency error
// ✅ Đúng - Sử dụng ModuleRef để resolve circular dependency
@Injectable()
export class AuthService {
constructor(private moduleRef: ModuleRef) {}
async validateUser(email: string, password: string) {
const usersService = this.moduleRef.get(UsersService);
const user = await usersService.findByEmail(email);
return user;
}
}
@Injectable()
export class UsersService {
constructor(private moduleRef: ModuleRef) {}
async findByEmail(email: string) {
const authService = this.moduleRef.get(AuthService);
// Use authService if needed
return { id: 1, email };
}
}
3. Lazy Loading Modules
// lazy.module.ts
@Module({
providers: [LazyService],
})
export class LazyModule {}
// app.service.ts
@Injectable()
export class AppService {
constructor(private moduleRef: ModuleRef) {}
async useLazyService() {
// Load module khi cần
const lazyModule = await this.moduleRef.load(LazyModule);
const lazyService = lazyModule.get(LazyService);
return lazyService.doSomething();
}
}
4. Dynamic Provider Resolution
// plugin.interface.ts
export interface Plugin {
execute(): void;
}
export const PLUGIN_TOKEN = 'PLUGIN';
// email-plugin.ts
@Injectable()
export class EmailPlugin implements Plugin {
execute() {
console.log('Email plugin executed');
}
}
// sms-plugin.ts
@Injectable()
export class SmsPlugin implements Plugin {
execute() {
console.log('SMS plugin executed');
}
}
// plugins.module.ts
@Module({
providers: [
{ provide: 'EMAIL_PLUGIN', useClass: EmailPlugin },
{ provide: 'SMS_PLUGIN', useClass: SmsPlugin },
],
exports: ['EMAIL_PLUGIN', 'SMS_PLUGIN'],
})
export class PluginsModule {}
// plugin-runner.service.ts
@Injectable()
export class PluginRunnerService {
constructor(private moduleRef: ModuleRef) {}
async runPlugins(pluginNames: string[]) {
const results = [];
for (const pluginName of pluginNames) {
const plugin = this.moduleRef.get<Plugin>(`${pluginName.toUpperCase()}_PLUGIN`);
if (plugin) {
plugin.execute();
results.push({
plugin: pluginName,
status: 'executed',
});
}
}
return results;
}
}
// app.controller.ts
@Controller('plugins')
export class AppController {
constructor(private pluginRunner: PluginRunnerService) {}
@Post('run')
async runPlugins(@Body('plugins') plugins: string[]) {
return this.pluginRunner.runPlugins(plugins);
}
}
5. Factory Pattern with ModuleRef
// strategies/strategy.interface.ts
export interface PaymentStrategy {
pay(amount: number): void;
}
// strategies/stripe.strategy.ts
@Injectable()
export class StripeStrategy implements PaymentStrategy {
pay(amount: number) {
console.log(`Paid ${amount} via Stripe`);
}
}
// strategies/paypal.strategy.ts
@Injectable()
export class PayPalStrategy implements PaymentStrategy {
pay(amount: number) {
console.log(`Paid ${amount} via PayPal`);
}
}
// payment.factory.ts
@Injectable()
export class PaymentFactory {
private strategies = {
stripe: StripeStrategy,
paypal: PayPalStrategy,
};
constructor(private moduleRef: ModuleRef) {}
getStrategy(name: string): PaymentStrategy {
const StrategyClass = this.strategies[name];
if (!StrategyClass) {
throw new BadRequestException(`Unknown payment strategy: ${name}`);
}
return this.moduleRef.get(StrategyClass);
}
}
// payment.service.ts
@Injectable()
export class PaymentService {
constructor(private paymentFactory: PaymentFactory) {}
processPayment(strategy: string, amount: number) {
const paymentStrategy = this.paymentFactory.getStrategy(strategy);
paymentStrategy.pay(amount);
return { success: true, amount, strategy };
}
}
// payment.controller.ts
@Controller('payment')
export class PaymentController {
constructor(private paymentService: PaymentService) {}
@Post()
processPayment(
@Body('strategy') strategy: string,
@Body('amount') amount: number,
) {
return this.paymentService.processPayment(strategy, amount);
}
}
6. Request-Scoped Providers
// request.service.ts
@Injectable({ scope: Scope.REQUEST })
export class RequestService {
private requestId: string = Math.random().toString();
getRequestId() {
return this.requestId;
}
}
// app.service.ts
@Injectable()
export class AppService {
constructor(private moduleRef: ModuleRef) {}
// Resolve REQUEST-scoped provider
async getRequestInfo() {
// Dùng resolve() cho REQUEST-scoped providers
const requestService = await this.moduleRef.resolve(RequestService);
return { requestId: requestService.getRequestId() };
}
}
7. Module Context Access
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
@Injectable()
export class AppService {
constructor(private moduleRef: ModuleRef) {}
getModuleInfo() {
// Get access to module
const module = this.moduleRef.get(UsersModule);
// Get all providers từ module
const providers = this.moduleRef.get(UsersModule);
return {
module,
providers,
};
}
// List tất cả available providers
listProviders() {
const providers = [];
try {
providers.push(this.moduleRef.get(UsersService));
providers.push(this.moduleRef.get(ProductsService));
providers.push(this.moduleRef.get(AuthService));
} catch (error) {
console.error('Some providers not available');
}
return providers.filter((p) => p !== undefined);
}
}
8. Conditional Service Resolution
@Injectable()
export class ConfigurableService {
constructor(
private moduleRef: ModuleRef,
private configService: ConfigService,
) {}
async getService(serviceName: string) {
const isProduction = this.configService.get('NODE_ENV') === 'production';
const serviceMap = isProduction
? {
database: DatabaseServiceProd,
cache: CacheServiceProd,
}
: {
database: DatabaseServiceDev,
cache: CacheServiceDev,
};
const ServiceClass = serviceMap[serviceName];
if (!ServiceClass) {
throw new NotFoundException(`Service ${serviceName} not found`);
}
return this.moduleRef.get(ServiceClass);
}
}
9. Service Registry Pattern
// service.registry.ts
interface ServiceRegistryEntry {
key: string;
provider: any;
description: string;
}
@Injectable()
export class ServiceRegistry {
private registry: Map<string, ServiceRegistryEntry> = new Map();
register(key: string, provider: any, description: string = '') {
this.registry.set(key, { key, provider, description });
}
get(key: string) {
const entry = this.registry.get(key);
if (!entry) {
throw new NotFoundException(`Service ${key} not found in registry`);
}
return entry.provider;
}
getAll() {
return Array.from(this.registry.values());
}
listServices() {
const services = [];
this.registry.forEach((entry) => {
services.push({
key: entry.key,
description: entry.description,
});
});
return services;
}
}
// app.module.ts
@Module({
providers: [
ServiceRegistry,
{
provide: 'INIT_SERVICE_REGISTRY',
useFactory: (registry: ServiceRegistry) => {
registry.register('users', UsersService, 'User management');
registry.register('products', ProductsService, 'Product management');
registry.register('orders', OrdersService, 'Order management');
return registry;
},
inject: [ServiceRegistry],
},
],
})
export class AppModule {}
// service-discovery.controller.ts
@Controller('services')
export class ServiceDiscoveryController {
constructor(private serviceRegistry: ServiceRegistry) {}
@Get()
listServices() {
return this.serviceRegistry.listServices();
}
@Get(':key')
getService(@Param('key') key: string) {
const service = this.serviceRegistry.get(key);
return { key, service };
}
}
10. Dynamic Service Creation
@Injectable()
export class ServiceFactory {
constructor(private moduleRef: ModuleRef) {}
async createUserService(config: any) {
// Create provider dynamically
const UserServiceDynamic = class {
constructor() {
this.config = config;
}
getConfig() {
return this.config;
}
};
// Register dynamically
return this.moduleRef.create(UserServiceDynamic);
}
async createWithDependencies(serviceClass: any) {
return await this.moduleRef.resolve(serviceClass);
}
}
get() vs resolve()
@Injectable()
export class AppService {
constructor(private moduleRef: ModuleRef) {}
// get() - Synchronous, returns cached instance
synchronousGet() {
const service = this.moduleRef.get(UsersService);
return service; // Singleton instance
}
// resolve() - Asynchronous, creates new instance for REQUEST-scoped
async asynchronousResolve() {
const service = await this.moduleRef.resolve(UsersService);
return service; // New instance for REQUEST-scoped
}
// REQUEST-scoped example
async exampleRequestScoped() {
// get() sẽ lỗi vì REQUEST-scoped không thể sync
// const service = this.moduleRef.get(RequestScopedService);
// Phải dùng resolve()
const service = await this.moduleRef.resolve(RequestScopedService);
return service;
}
}
Best Practices
1. Tránh Overuse của ModuleRef
// ❌ Sai - Overuse ModuleRef
@Injectable()
export class BadService {
constructor(private moduleRef: ModuleRef) {}
doSomething() {
const serviceA = this.moduleRef.get(ServiceA);
const serviceB = this.moduleRef.get(ServiceB);
return serviceA.method() + serviceB.method();
}
}
// ✅ Đúng - Dùng direct injection khi có thể
@Injectable()
export class GoodService {
constructor(
private serviceA: ServiceA,
private serviceB: ServiceB,
) {}
doSomething() {
return this.serviceA.method() + this.serviceB.method();
}
}
2. Handle Not Found Cases
// ✅ Đúng - Check existence trước
@Injectable()
export class SafeService {
constructor(private moduleRef: ModuleRef) {}
getService(name: string) {
try {
const service = this.moduleRef.get(name, { strict: false });
if (!service) {
throw new NotFoundException(`Service ${name} not found`);
}
return service;
} catch (error) {
console.error('Error resolving service', error);
throw error;
}
}
}
3. Use Appropriate Method
@Injectable()
export class AppService {
constructor(private moduleRef: ModuleRef) {}
// Sử dụng get() cho SINGLETON (nhanh, cached)
getSingletonService() {
return this.moduleRef.get(SingletonService);
}
// Sử dụng resolve() cho REQUEST-scoped
async getRequestService() {
return await this.moduleRef.resolve(RequestService);
}
// Sử dụng create() cho TRANSIENT
getNewInstance() {
return this.moduleRef.create(TransientService);
}
}
4. Type Safety
// ❌ Sai - No type safety
const service = this.moduleRef.get('users');
// ✅ Đúng - Type-safe
const service = this.moduleRef.get(UsersService);
// Hoặc với generic
const service = this.moduleRef.get<UsersService>(UsersService);
5. Error Handling
@Injectable()
export class SafeAppService {
constructor(private moduleRef: ModuleRef) {}
safeGet(serviceClass: any) {
try {
return this.moduleRef.get(serviceClass);
} catch (error) {
console.error(`Failed to get ${serviceClass.name}`, error);
return null;
}
}
async safeResolve(serviceClass: any) {
try {
return await this.moduleRef.resolve(serviceClass);
} catch (error) {
console.error(`Failed to resolve ${serviceClass.name}`, error);
return null;
}
}
}
Complete Example
// services/notification.service.ts
export interface NotificationStrategy {
send(message: string): void;
}
@Injectable()
export class EmailNotification implements NotificationStrategy {
send(message: string) {
console.log(`Email: ${message}`);
}
}
@Injectable()
export class SmsNotification implements NotificationStrategy {
send(message: string) {
console.log(`SMS: ${message}`);
}
}
// services/notification-manager.service.ts
@Injectable()
export class NotificationManager {
private strategies = {
email: EmailNotification,
sms: SmsNotification,
};
constructor(private moduleRef: ModuleRef) {}
sendNotification(type: string, message: string) {
const StrategyClass = this.strategies[type];
if (!StrategyClass) {
throw new BadRequestException(`Unknown notification type: ${type}`);
}
const strategy = this.moduleRef.get<NotificationStrategy>(StrategyClass);
strategy.send(message);
return {
success: true,
type,
message,
timestamp: new Date(),
};
}
getSupportedTypes() {
return Object.keys(this.strategies);
}
}
// controllers/notification.controller.ts
@Controller('notifications')
export class NotificationController {
constructor(private notificationManager: NotificationManager) {}
@Post('send')
send(
@Body('type') type: string,
@Body('message') message: string,
) {
return this.notificationManager.sendNotification(type, message);
}
@Get('types')
getSupportedTypes() {
return this.notificationManager.getSupportedTypes();
}
}
// app.module.ts
@Module({
controllers: [NotificationController],
providers: [
NotificationManager,
EmailNotification,
SmsNotification,
],
})
export class AppModule {}
Kết Luận
Module Reference là công cụ mạnh mẽ để:
- Resolve providers dynamically tại runtime
- Tránh circular dependencies
- Implement factory patterns
- Lazy load providers
- Create flexible architectures
Sử dụng Module Reference đúng cách giúp bạn:
- Xây dựng scalable architectures
- Giảm coupling giữa modules
- Implement complex patterns
- Tạo plugin systems
- Flexible service resolution
- Advanced dependency management
Tuy nhiên, hãy sử dụng Module Reference một cách cẩn thận - trong hầu hết trường hợp, dependency injection truyền thống là giải pháp tốt hơn!