Skip to main content

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:

  1. Intercept incoming request trước khi tới controller
  2. Transform response sau khi controller xử lý xong
  3. Add extra logic xung quanh method execution
  4. 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

FeatureInterceptorsGuardsPipesMiddleware
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