Interceptors trong NestJS
Interceptors là một công cụ mạnh mẽ cho phép bạn intercept (chặn) method calls và modify request/response. Một interceptor là một class implement interface NestInterceptor. Interceptors được thực thi sau guards nhưng trước pipes.
Khái Niệm Interceptor
Interceptor có khả năng:
- Intercept incoming request trước khi tới controller
- Transform response sau khi controller xử lý xong
- Add extra logic xung quanh method execution
- Handle exceptions throw ra từ method
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
Request Processing Pipeline (with Interceptors)
Request
↓
Global Middleware
↓
Module Middleware
↓
Guards
↓
→ Interceptors (before)
↓
Pipes (validation)
↓
Controller
↓
→ Interceptors (after)
↓
Response
CallHandler và Observable
Interceptor nhận CallHandler mà có method handle() trả về Observable:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, map, catchError } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Trước khi handler được gọi
console.log('Request received');
return next.handle().pipe(
// Sau khi handler thực thi xong
map((data) => {
console.log('Response sent');
return { data, timestamp: new Date() };
}),
// Catch exceptions
catchError((error) => {
console.error('Error occurred', error);
throw error;
}),
);
}
}
Ví Dụ Interceptors Thực Tế
1. Logging Interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HttpRequest');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();
const { method, originalUrl, ip, headers } = request;
const userAgent = headers['user-agent'];
const startTime = Date.now();
this.logger.log(`Incoming Request: ${method} ${originalUrl}`);
this.logger.log(`IP: ${ip}, User-Agent: ${userAgent}`);
return next.handle().pipe(
tap(() => {
const duration = Date.now() - startTime;
this.logger.log(
`Outgoing Response: ${method} ${originalUrl} - ${response.statusCode} - ${duration}ms`,
);
}),
);
}
}
2. Response Transformation Interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
statusCode: number;
message: string;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
return next.handle().pipe(
map((data) => ({
statusCode: context.switchToHttp().getResponse().statusCode,
message: 'Success',
data,
timestamp: new Date().toISOString(),
})),
);
}
}
// Sử dụng
@Controller('users')
@UseInterceptors(TransformInterceptor)
export class UsersController {
@Get()
findAll() {
return ['Alice', 'Bob']; // Sẽ tự động wrapped
}
}
// Response:
// {
// "statusCode": 200,
// "message": "Success",
// "data": ["Alice", "Bob"],
// "timestamp": "2024-02-28T10:00:00.000Z"
// }
3. Error Handling Interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpException } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorHandlingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((error) => {
const response = context.switchToHttp().getResponse();
let statusCode = 500;
let message = 'Internal server error';
if (error instanceof HttpException) {
statusCode = error.getStatus();
const exceptionResponse = error.getResponse();
message = typeof exceptionResponse === 'object'
? (exceptionResponse as any).message
: exceptionResponse;
}
return throwError(
() =>
new HttpException(
{
statusCode,
message,
timestamp: new Date().toISOString(),
path: context.switchToHttp().getRequest().url,
},
statusCode,
),
);
}),
);
}
}
4. Request Modification Interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
@Injectable()
export class RequestModificationInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
// Trim whitespace từ query parameters
Object.keys(request.query).forEach((key) => {
if (typeof request.query[key] === 'string') {
request.query[key] = (request.query[key] as string).trim();
}
});
// Trim whitespace từ request body
if (request.body && typeof request.body === 'object') {
Object.keys(request.body).forEach((key) => {
if (typeof request.body[key] === 'string') {
request.body[key] = request.body[key].trim();
}
});
}
// Lowercase email
if (request.body?.email) {
request.body.email = request.body.email.toLowerCase();
}
return next.handle();
}
}
5. Caching Interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Request } from 'express';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class CachingInterceptor implements NestInterceptor {
private cache = new Map<string, any>();
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
// Chỉ cache GET requests
if (request.method !== 'GET') {
return next.handle();
}
const cacheKey = `${request.method}:${request.url}`;
// Nếu có trong cache, trả về cache
if (this.cache.has(cacheKey)) {
const cachedData = this.cache.get(cacheKey);
console.log(`Cache hit for ${cacheKey}`);
return of(cachedData);
}
// Nếu không, gọi handler và lưu kết quả vào cache
return next.handle().pipe(
tap((data) => {
console.log(`Cache set for ${cacheKey}`);
this.cache.set(cacheKey, data);
// Clear cache sau 5 phút
setTimeout(() => {
this.cache.delete(cacheKey);
}, 5 * 60 * 1000);
}),
);
}
}
6. Compression Interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import * as zlib from 'zlib';
import { promisify } from 'util';
const gzip = promisify(zlib.gzip);
@Injectable()
export class CompressionInterceptor implements NestInterceptor {
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const shouldCompress = request.headers['accept-encoding']?.includes('gzip');
if (!shouldCompress) {
return next.handle();
}
return next.handle().pipe(
map(async (data) => {
const json = JSON.stringify(data);
const compressed = await gzip(json);
return compressed;
}),
);
}
}
7. Performance Monitoring Interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
private readonly logger = new Logger('Performance');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();
const startTime = performance.now();
const startMemory = process.memoryUsage();
return next.handle().pipe(
tap(() => {
const endTime = performance.now();
const endMemory = process.memoryUsage();
const duration = endTime - startTime;
const memoryDelta = {
heapUsed: endMemory.heapUsed - startMemory.heapUsed,
heapTotal: endMemory.heapTotal - startMemory.heapTotal,
};
const performanceLog = {
method: request.method,
path: request.path,
statusCode: response.statusCode,
duration: `${duration.toFixed(2)}ms`,
memoryDelta,
};
if (duration > 1000) {
this.logger.warn(`Slow request detected: ${JSON.stringify(performanceLog)}`);
} else {
this.logger.debug(`Request completed: ${JSON.stringify(performanceLog)}`);
}
}),
);
}
}
8. Timeout Interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable } from 'rxjs';
import { timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(30000), // 30 second timeout
);
}
}
// Hoặc custom timeout per route
export const CustomTimeout = (ms: number) => SetMetadata('timeout', ms);
@Injectable()
export class CustomTimeoutInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const customTimeout = this.reflector.get<number>('timeout', context.getHandler());
const timeoutMs = customTimeout || 30000;
return next.handle().pipe(timeout(timeoutMs));
}
}
9. Request/Response Body Logging Interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Request, Response } from 'express';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class BodyLoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();
console.log(`\n=== REQUEST ===`);
console.log(`Method: ${request.method}`);
console.log(`URL: ${request.url}`);
console.log(`Body:`, request.body);
console.log(`Query:`, request.query);
return next.handle().pipe(
tap((data) => {
console.log(`\n=== RESPONSE ===`);
console.log(`Status: ${response.statusCode}`);
console.log(`Data:`, data);
console.log(`==================\n`);
}),
);
}
}
10. Circular Reference Handling Interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class CircularReferenceInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
return this.removeCircularReferences(data);
}),
);
}
private removeCircularReferences(obj: any, seen = new WeakSet()): any {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (seen.has(obj)) {
return '[Circular]';
}
seen.add(obj);
if (Array.isArray(obj)) {
return obj.map((item) => this.removeCircularReferences(item, seen));
}
const result: any = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = this.removeCircularReferences(obj[key], seen);
}
}
return result;
}
}
Interceptors at Different Scopes
Method-level
@Controller('users')
export class UsersController {
@Get()
@UseInterceptors(LoggingInterceptor)
findAll() {
return [];
}
@Post()
@UseInterceptors(TransformInterceptor, LoggingInterceptor)
create(@Body() createUserDto: CreateUserDto) {
return { message: 'User created' };
}
}
Class-level
@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
// Tất cả methods trong controller được apply interceptor
@Get()
findAll() {
return [];
}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return { message: 'User created' };
}
}
Global-level
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
await app.listen(3000);
}
bootstrap();
Module-level (Provider)
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
Async Interceptors
Interceptors có thể async:
@Injectable()
export class AsyncInterceptor implements NestInterceptor {
constructor(private configService: ConfigService) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const isProduction = await this.configService.get('NODE_ENV') === 'production';
if (isProduction) {
// Do something in production
}
return next.handle();
}
}
Combining Multiple Interceptors
Interceptors được thực thi theo thứ tự khai báo:
@Controller('users')
@UseInterceptors(
LoggingInterceptor, // 1
TransformInterceptor, // 2
ErrorHandlingInterceptor, // 3
)
export class UsersController {
@Get()
findAll() {
return [];
}
}
// Request flow:
// Request
// ↓
// LoggingInterceptor (before)
// ↓
// TransformInterceptor (before)
// ↓
// ErrorHandlingInterceptor (before)
// ↓
// Handler execution
// ↓
// ErrorHandlingInterceptor (after)
// ↓
// TransformInterceptor (after)
// ↓
// LoggingInterceptor (after)
// ↓
// Response
RxJS Operators in Interceptors
import { tap, map, catchError, timeout, retry } from 'rxjs/operators';
@Injectable()
export class AdvancedInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
// Retry 3 lần nếu fail
retry(3),
// Timeout sau 10 giây
timeout(10000),
// Transform response
map((data) => ({ data, timestamp: new Date() })),
// Tap vào successful response
tap((data) => console.log('Success:', data)),
// Catch errors
catchError((error) => {
console.error('Error:', error);
throw error;
}),
);
}
}
Best Practices
1. Tách Biệt Concerns
// ❌ Sai - Quá nhiều logic trong một interceptor
@Injectable()
export class BadInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Logging
// Transformation
// Caching
// Error handling
// Performance monitoring
return next.handle();
}
}
// ✅ Đúng - Một interceptor một trách nhiệm
@UseInterceptors(
LoggingInterceptor,
TransformInterceptor,
CachingInterceptor,
ErrorHandlingInterceptor,
PerformanceInterceptor,
)
export class UserController {}
2. Use Observable Operators Properly
// ✅ Đúng - Sử dụng RxJS operators
@Injectable()
export class GoodInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
retry(2),
map((data) => ({ data })),
catchError((error) => throwError(() => error)),
);
}
}
3. Avoid Heavy Operations
// ❌ Sai - Nặng trong interceptor
@Injectable()
export class BadInterceptor implements NestInterceptor {
constructor(private db: DatabaseService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Lấy toàn bộ data từ database
const allUsers = this.db.getAllUsers();
return next.handle();
}
}
// ✅ Đúng - Chỉ những gì cần thiết
@Injectable()
export class GoodInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Chỉ log request info
const request = context.switchToHttp().getRequest();
console.log(`${request.method} ${request.path}`);
return next.handle();
}
}
4. Handle Errors Properly
// ✅ Đúng - Error handling
@Injectable()
export class ErrorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((error) => {
console.error('Request error:', error);
// Re-throw hoặc transform error
throw error;
}),
);
}
}
5. Type Safety
// ✅ Đúng - Generic types
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { data: T }> {
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<{ data: T }> {
return next.handle().pipe(
map((data) => ({ data })),
);
}
}
Complete Example
// interceptors/response.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor<T, any> {
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
return next.handle().pipe(
tap(() => {
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`);
}),
map((data) => ({
statusCode: response.statusCode,
message: 'Success',
data,
timestamp: new Date().toISOString(),
})),
);
}
}
// interceptors/error.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpException } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((error) => {
const response = context.switchToHttp().getResponse();
let statusCode = 500;
let message = 'Internal server error';
if (error instanceof HttpException) {
statusCode = error.getStatus();
message = error.message;
}
return throwError(
() =>
new HttpException(
{
statusCode,
message,
timestamp: new Date().toISOString(),
},
statusCode,
),
);
}),
);
}
}
// users.controller.ts
@Controller('users')
@UseInterceptors(ResponseInterceptor, ErrorInterceptor)
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}
// app.module.ts
@Module({
controllers: [UsersController],
providers: [
UsersService,
{
provide: APP_INTERCEPTOR,
useClass: ResponseInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: ErrorInterceptor,
},
],
})
export class AppModule {}
Interceptors vs Other Features
| Feature | Interceptors | Guards | Pipes | Middleware |
|---|---|---|---|---|
| Execution Context | ✓ | ✓ | ✓ | ✗ |
| Transform Request | ✓ | ✓ | ✓ | ✓ |
| Transform Response | ✓ | ✗ | ✗ | ✓ |
| Caching | ✓ | ✗ | ✗ | ✗ |
| Logging | ✓ | ✗ | ✗ | ✓ |
| Error Handling | ✓ | ✗ | ✗ | ✗ |
| RxJS Support | ✓ | ✗ | ✗ | ✗ |
Kết Luận
Interceptors là công cụ mạnh mẽ để:
- Transform requests và responses
- Log requests/responses
- Cache results
- Handle errors consistently
- Monitor performance
- Modify behavior của applications
Sử dụng Interceptors đúng cách giúp bạn:
- Tách biệt concerns một cách rõ ràng
- Tái sử dụng logic xuyên các routes
- Có khả năng bắt và modify requests/responses
- Tạo consistent response format
- Cải thiện debugging và monitoring
- Xây dựng ứng dụng maintainable và scalable