Injection Scopes trong NestJS
Injection Scopes xác định vòng đời và số lượng instances của một provider. NestJS cung cấp ba scope khác nhau, mỗi cái có use cases và performance implications khác nhau. Hiểu rõ scopes là quan trọng để tối ưu hóa performance và tránh memory leaks.
Các Loại Scopes
1. SINGLETON (Default)
Một instance duy nhất được tạo một lần và chia sẻ cho toàn bộ ứng dụng:
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.SINGLETON })
export class SingletonService {
private count = 0;
increment() {
return ++this.count;
}
getCount() {
return this.count;
}
}
// Module
@Module({
providers: [SingletonService],
})
export class AppModule {}
// Controller
@Controller('singleton')
export class SingletonController {
constructor(private service: SingletonService) {}
@Get('increment')
increment() {
return { count: this.service.increment() }; // count tăng cộng dồn
}
@Get('status')
getStatus() {
return { count: this.service.getCount() }; // Trả về giá trị tích lũy
}
}
// Request 1: /increment → { count: 1 }
// Request 2: /increment → { count: 2 }
// Request 3: /status → { count: 2 }
Đặc điểm:
- ✅ Best performance (instance được reuse)
- ✅ Memory efficient
- ❌ Chia sẻ state giữa requests
- ❌ Không thread-safe (trong context của multiple requests)
Khi sử dụng:
- Stateless services (DatabaseService, ConfigService)
- Utilities và helpers
- Heavy initialization services
2. REQUEST
Một instance mới được tạo cho mỗi HTTP request:
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
private requestId = Math.random().toString(36).substring(7);
getRequestId() {
return this.requestId;
}
}
// Controller
@Controller('request')
export class RequestController {
constructor(private service: RequestScopedService) {}
@Get('id')
getId() {
return { requestId: this.service.getRequestId() };
}
@Get('id2')
getId2() {
return { requestId: this.service.getRequestId() }; // Cùng request, cùng ID
}
}
// Request 1: /id → { requestId: "abc123" }
// Request 2: /id → { requestId: "def456" } (Request mới, ID mới)
Đặc điểm:
- ✅ Isolation giữa requests
- ✅ Request-scoped state an toàn
- ❌ Tốn memory hơn
- ❌ Performance overhead từ tạo instances mới
Khi sử dụng:
- Request-scoped utilities
- Temporary data holders
- Request context managers
- Session-specific services
3. TRANSIENT
Một instance mới được tạo mỗi khi được inject:
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {
private id = Math.random().toString(36).substring(7);
getId() {
return this.id;
}
}
// Service
@Injectable()
export class MyService {
constructor(
private transient1: TransientService,
private transient2: TransientService,
) {}
getIds() {
return {
id1: this.transient1.getId(),
id2: this.transient2.getId(),
same: this.transient1 === this.transient2, // false!
};
}
}
// Response: { id1: "abc123", id2: "def456", same: false }
Đặc điểm:
- ✅ Hoàn toàn isolated instances
- ✅ Không chia sẻ state
- ❌ Tốn memory nhiều nhất
- ❌ Performance worst case
Khi sử dụng:
- Hiếm khi sử dụng
- Khi bạn cần hoàn toàn isolated state
- Một số specialized use cases
Scope Comparison
| Aspect | SINGLETON | REQUEST | TRANSIENT |
|---|---|---|---|
| Instances created | 1 (startup) | 1 per request | 1 per injection |
| Memory usage | Minimal | Medium | High |
| Performance | Best | Good | Poor |
| State isolation | None | Per-request | Per-instance |
| Thread safety | ❌ | ✅ | ✅ |
| Reusable | ✅ | ✅ | ❌ |
Ví Dụ Thực Tế
1. Request Context Service (REQUEST scoped)
// request-context.ts
@Injectable({ scope: Scope.REQUEST })
export class RequestContext {
private context: {
userId?: string;
requestId: string;
startTime: number;
} = {
requestId: Math.random().toString(36).substring(7),
startTime: Date.now(),
};
setUserId(userId: string) {
this.context.userId = userId;
}
getContext() {
return this.context;
}
getDuration() {
return Date.now() - this.context.startTime;
}
}
// middleware
@Injectable()
export class ContextMiddleware implements NestMiddleware {
constructor(private context: RequestContext) {}
use(req: Request, res: Response, next: NextFunction) {
const user = (req as any).user;
if (user) {
this.context.setUserId(user.id);
}
next();
}
}
// services
@Injectable()
export class UsersService {
constructor(private context: RequestContext) {}
async getUser(id: string) {
console.log(`[${this.context.getContext().requestId}] Fetching user ${id}`);
return { id, name: 'John' };
}
}
// controller
@Controller('users')
export class UsersController {
constructor(
private usersService: UsersService,
private context: RequestContext,
) {}
@Get(':id')
async getUser(@Param('id') id: string) {
const user = await this.usersService.getUser(id);
return {
user,
requestId: this.context.getContext().requestId,
duration: this.context.getDuration(),
};
}
}
2. Database Transaction (REQUEST scoped)
// transaction.service.ts
@Injectable({ scope: Scope.REQUEST })
export class TransactionService {
private transaction: any;
private isCommitted = false;
async begin() {
console.log('Beginning transaction');
this.transaction = {
statements: [],
};
}
async addStatement(sql: string) {
if (!this.transaction) {
throw new Error('Transaction not started');
}
this.transaction.statements.push(sql);
}
async commit() {
console.log('Committing transaction');
this.isCommitted = true;
return this.transaction;
}
async rollback() {
console.log('Rolling back transaction');
this.transaction = null;
}
isActive() {
return !!this.transaction && !this.isCommitted;
}
}
// middleware
@Injectable()
export class TransactionMiddleware implements NestMiddleware {
constructor(private transaction: TransactionService) {}
async use(req: Request, res: Response, next: NextFunction) {
await this.transaction.begin();
// Commit on success
const originalSend = res.send;
res.send = function(data) {
if (res.statusCode < 400) {
this.transaction.commit();
} else {
this.transaction.rollback();
}
return originalSend.call(this, data);
};
next();
}
}
// service
@Injectable()
export class OrderService {
constructor(private transaction: TransactionService) {}
async createOrder(orderData: any) {
await this.transaction.addStatement(
`INSERT INTO orders VALUES (...)`,
);
await this.transaction.addStatement(
`INSERT INTO order_items VALUES (...)`,
);
return { orderId: '123' };
}
}
3. Logger with Request Tracking (REQUEST scoped)
// logger.service.ts
@Injectable({ scope: Scope.REQUEST })
export class LoggerService {
private requestId = Math.random().toString(36).substring(7);
private logs: Array<{ level: string; message: string; time: string }> = [];
log(message: string) {
const entry = {
level: 'INFO',
message,
time: new Date().toISOString(),
};
this.logs.push(entry);
console.log(`[${this.requestId}] ${message}`);
}
error(message: string) {
const entry = {
level: 'ERROR',
message,
time: new Date().toISOString(),
};
this.logs.push(entry);
console.error(`[${this.requestId}] ${message}`);
}
getLogs() {
return {
requestId: this.requestId,
logs: this.logs,
};
}
}
// interceptor
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
constructor(private logger: LoggerService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const handler = context.getHandler();
this.logger.log(`→ ${request.method} ${request.path}`);
this.logger.log(`Handler: ${handler.name}`);
return next.handle().pipe(
tap(() => {
this.logger.log(`← Request completed`);
}),
catchError((error) => {
this.logger.error(`✗ Error: ${error.message}`);
throw error;
}),
);
}
}
// controller
@Controller('debug')
export class DebugController {
constructor(private logger: LoggerService) {}
@Get('logs')
getLogs() {
return this.logger.getLogs();
}
}
4. Data Loader (REQUEST scoped)
// data-loader.ts
@Injectable({ scope: Scope.REQUEST })
export class DataLoader {
private cache: Map<string, any> = new Map();
private batchQueue: Map<string, Set<string>> = new Map();
async load(resource: string, id: string) {
const cacheKey = `${resource}:${id}`;
// Return từ cache nếu đã load
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
// Queue for batch loading
if (!this.batchQueue.has(resource)) {
this.batchQueue.set(resource, new Set());
}
this.batchQueue.get(resource)!.add(id);
// Batch load at end of request
const result = await this.batchLoad(resource, id);
this.cache.set(cacheKey, result);
return result;
}
private async batchLoad(resource: string, id: string) {
// Simulate batch database query
return { id, data: `Resource ${resource}#${id}` };
}
}
// service
@Injectable()
export class GraphQLService {
constructor(private dataLoader: DataLoader) {}
async resolveUser(userId: string) {
return this.dataLoader.load('user', userId);
}
async resolvePost(postId: string) {
return this.dataLoader.load('post', postId);
}
}
5. Audit Logger (REQUEST scoped)
// audit-logger.ts
@Injectable({ scope: Scope.REQUEST })
export class AuditLogger {
private requestId = Math.random().toString(36).substring(7);
private userId?: string;
private auditEvents: any[] = [];
setUserId(userId: string) {
this.userId = userId;
}
logEvent(action: string, resource: string, details?: any) {
const event = {
requestId: this.requestId,
userId: this.userId,
action,
resource,
timestamp: new Date().toISOString(),
details,
};
this.auditEvents.push(event);
console.log(`[AUDIT] ${JSON.stringify(event)}`);
}
flush() {
// Save to audit database/log
return this.auditEvents;
}
}
// interceptor
@Injectable()
export class AuditInterceptor implements NestInterceptor {
constructor(private auditLogger: AuditLogger) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const handler = context.getHandler();
this.auditLogger.logEvent('API_CALL', handler.name, {
method: request.method,
path: request.path,
});
return next.handle().pipe(
tap(() => {
this.auditLogger.logEvent('API_SUCCESS', handler.name);
}),
catchError((error) => {
this.auditLogger.logEvent('API_ERROR', handler.name, {
error: error.message,
});
throw error;
}),
);
}
}
6. Session Service (REQUEST scoped)
// session.service.ts
@Injectable({ scope: Scope.REQUEST })
export class SessionService {
private sessionData: Map<string, any> = new Map();
private sessionId = Math.random().toString(36).substring(7);
set(key: string, value: any) {
this.sessionData.set(key, value);
}
get(key: string) {
return this.sessionData.get(key);
}
getAll() {
return Object.fromEntries(this.sessionData);
}
getSessionId() {
return this.sessionId;
}
}
// middleware
@Injectable()
export class SessionMiddleware implements NestMiddleware {
constructor(private session: SessionService) {}
use(req: Request, res: Response, next: NextFunction) {
const user = (req as any).user;
if (user) {
this.session.set('user', user);
this.session.set('loginTime', new Date());
}
next();
}
}
// service
@Injectable()
export class ProfileService {
constructor(private session: SessionService) {}
getProfile() {
const user = this.session.get('user');
const loginTime = this.session.get('loginTime');
return {
user,
loginTime,
sessionId: this.session.getSessionId(),
};
}
}
7. Performance Monitoring (REQUEST scoped)
// performance-monitor.ts
@Injectable({ scope: Scope.REQUEST })
export class PerformanceMonitor {
private marks: Map<string, number> = new Map();
private startTime = Date.now();
mark(label: string) {
this.marks.set(label, Date.now() - this.startTime);
}
measure(label: string): number {
if (!this.marks.has(label)) {
return -1;
}
return Date.now() - this.startTime - this.marks.get(label)!;
}
getMetrics() {
return {
totalDuration: Date.now() - this.startTime,
marks: Object.fromEntries(this.marks),
};
}
}
// interceptor
@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
constructor(private monitor: PerformanceMonitor) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
this.monitor.mark('request_start');
return next.handle().pipe(
tap(() => {
this.monitor.mark('handler_complete');
const duration = this.monitor.measure('handler_complete');
console.log(`Handler completed in ${duration}ms`);
}),
);
}
}
8. Switching Scopes Dynamically
// Không thể inject REQUEST vào SINGLETON!
@Injectable({ scope: Scope.SINGLETON })
export class BadService {
// ❌ LỖI - Cannot inject REQUEST scoped into SINGLETON
constructor(private request: RequestService) {}
}
// ✅ Đúng - Sử dụng ModuleRef để get REQUEST-scoped service
@Injectable({ scope: Scope.SINGLETON })
export class GoodService {
constructor(private moduleRef: ModuleRef) {}
async executeInRequestContext() {
const requestService = await this.moduleRef.resolve(RequestService);
return requestService.doSomething();
}
}
Scope Rules and Constraints
Scope Compatibility
// ✅ SINGLETON có thể inject SINGLETON
@Injectable({ scope: Scope.SINGLETON })
export class Service1 {
constructor(private service2: Service2) {} // Scope.SINGLETON
}
// ✅ REQUEST có thể inject SINGLETON
@Injectable({ scope: Scope.REQUEST })
export class Service3 {
constructor(private service1: Service1) {} // Scope.SINGLETON
}
// ✅ REQUEST có thể inject REQUEST
@Injectable({ scope: Scope.REQUEST })
export class Service4 {
constructor(private service3: Service3) {} // Scope.REQUEST
}
// ❌ SINGLETON KHÔNG thể inject REQUEST
@Injectable({ scope: Scope.SINGLETON })
export class BadService {
constructor(private service3: Service3) {} // LỖI! Scope.REQUEST
}
// ✅ Workaround - Sử dụng ModuleRef
@Injectable({ scope: Scope.SINGLETON })
export class GoodService {
constructor(private moduleRef: ModuleRef) {}
async getRequestService() {
return this.moduleRef.resolve(Service3); // Scope.REQUEST
}
}
Best Practices
1. Use SINGLETON by Default
// ✅ Đúng - SINGLETON cho stateless services
@Injectable() // Default is SINGLETON
export class UsersRepository {
async findOne(id: string) {
return { id, name: 'John' };
}
}
// ❌ Sai - Không cần REQUEST scope nếu stateless
@Injectable({ scope: Scope.REQUEST })
export class UsersRepository {
async findOne(id: string) {
return { id, name: 'John' };
}
}
2. Use REQUEST for Per-Request State
// ✅ Đúng - REQUEST scope cho per-request state
@Injectable({ scope: Scope.REQUEST })
export class RequestContext {
private userId?: string;
setUserId(userId: string) {
this.userId = userId;
}
getUserId() {
return this.userId;
}
}
3. Avoid TRANSIENT
// ❌ Sai - TRANSIENT rarely needed
@Injectable({ scope: Scope.TRANSIENT })
export class UnnecessaryTransient {
execute() {
return 'result';
}
}
// ✅ Đúng - Use SINGLETON unless you have specific reason
@Injectable()
export class StatelessService {
execute() {
return 'result';
}
}
4. Document Scope Selection
/**
* Per-request service for tracking request-specific data
*
* @scope REQUEST - New instance created for each HTTP request
* @reason Maintains isolation between concurrent requests
* @example
* constructor(private context: RequestContext) {}
*/
@Injectable({ scope: Scope.REQUEST })
export class RequestContext {}
5. Be Aware of Scope Injection Rules
// ✅ Correct scope hierarchy
@Injectable() // SINGLETON
export class SingletonService {
constructor(private singleton: AnotherSingleton) {} // OK
}
@Injectable({ scope: Scope.REQUEST }) // REQUEST
export class RequestService {
constructor(
private singleton: SingletonService, // OK
private request: AnotherRequestService, // OK
) {}
}
// ❌ Wrong scope hierarchy
@Injectable() // SINGLETON
export class BadService {
constructor(private request: RequestService) {} // LỖI!
}
Complete Example: Full-Featured Service
// context.service.ts
/**
* Per-request service containing request context
* - Tracks request ID
* - Manages user information
* - Logs request lifecycle
*/
@Injectable({ scope: Scope.REQUEST })
export class ContextService {
private requestId = Math.random().toString(36).substring(7);
private userId?: string;
private startTime = Date.now();
private events: any[] = [];
setUserId(userId: string) {
this.userId = userId;
}
logEvent(action: string, details?: any) {
this.events.push({
action,
timestamp: new Date().toISOString(),
details,
});
}
getContext() {
return {
requestId: this.requestId,
userId: this.userId,
duration: Date.now() - this.startTime,
events: this.events,
};
}
}
// middleware
@Injectable()
export class ContextMiddleware implements NestMiddleware {
constructor(private context: ContextService) {}
use(req: Request, res: Response, next: NextFunction) {
const user = (req as any).user;
if (user) {
this.context.setUserId(user.id);
}
this.context.logEvent('REQUEST_START', {
method: req.method,
path: req.path,
});
res.on('finish', () => {
this.context.logEvent('REQUEST_END', {
statusCode: res.statusCode,
});
});
next();
}
}
// service
@Injectable()
export class UsersService {
constructor(
private usersRepository: UsersRepository,
private context: ContextService,
) {}
async getUser(id: string) {
this.context.logEvent('GET_USER_START', { userId: id });
const user = await this.usersRepository.findOne(id);
this.context.logEvent('GET_USER_END', { found: !!user });
return user;
}
}
// controller
@Controller('users')
export class UsersController {
constructor(
private usersService: UsersService,
private context: ContextService,
) {}
@Get(':id')
async getUser(@Param('id') id: string) {
const user = await this.usersService.getUser(id);
return {
user,
context: this.context.getContext(),
};
}
@Get('debug/context')
getContext() {
return this.context.getContext();
}
}
// module
@Module({
controllers: [UsersController],
providers: [UsersService, ContextService],
})
export class UsersModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(ContextMiddleware).forRoutes('*');
}
}
Kết Luận
Injection Scopes là fundamental concept trong NestJS:
SINGLETON:
- Best performance
- Ideal cho stateless services
- Default choice
REQUEST:
- Per-request isolation
- Ideal cho request-specific state
- Small performance overhead
TRANSIENT:
- Rarely used
- Each injection gets new instance
- High memory overhead
Key Takeaways:
- Default to SINGLETON
- Use REQUEST cho per-request state
- Avoid TRANSIENT
- Be aware of scope compatibility rules
- Request-scoped services cannot be injected into singletons
Sử dụng scopes đúng cách là crucial cho xây dựng efficient, safe, và scalable NestJS applications!