Interceptor là một class được đánh dấu bằng decorator @Injectable(), implement interface NestInterceptor.

Interceptor sở hữu một tập hợp các khả năng hữu ích, lấy cảm hứng từ kỹ thuật Aspect Oriented Programming (AOP). Chúng giúp bạn có thể:
- gắn thêm logic trước / sau khi method được thực thi
- chuyển đổi (transform) kết quả trả về từ một hàm
- chuyển đổi exception được throw ra từ một hàm
- mở rộng hành vi cơ bản của hàm
- ghi đè hoàn toàn một hàm tùy theo điều kiện cụ thể (ví dụ: cho mục đích caching)
Kiến thức cơ bản
Mỗi interceptor implement phương thức intercept(), nhận hai tham số. Tham số đầu tiên là instance ExecutionContext (chính xác là cùng một object như ở guard). ExecutionContext kế thừa từ ArgumentsHost. Chúng ta đã thấy ArgumentsHost trước đó ở chương exception filter. Ở đó, chúng ta thấy nó là một wrapper bao quanh các tham số được truyền vào handler gốc, và chứa các mảng tham số khác nhau tùy theo loại ứng dụng. Bạn có thể xem lại exception filters để biết thêm về chủ đề này.
Ngữ cảnh thực thi (Execution context)
Bằng cách kế thừa ArgumentsHost, ExecutionContext cũng bổ sung thêm một số helper method mới cung cấp chi tiết hơn về quá trình thực thi hiện tại. Những chi tiết này có thể hữu ích khi xây dựng các interceptor tổng quát hơn, có thể hoạt động trên nhiều controller, method và execution context khác nhau. Tìm hiểu thêm về ExecutionContext tại đây.
Call handler
Tham số thứ hai là một CallHandler. Interface CallHandler implement phương thức handle(), mà bạn có thể dùng để gọi route handler tại một thời điểm nào đó trong interceptor của mình. Nếu bạn không gọi phương thức handle() trong implementation của phương thức intercept(), route handler sẽ hoàn toàn không được thực thi.
Cách tiếp cận này có nghĩa là phương thức intercept() thực chất bao bọc (wrap) luồng request/response. Kết quả là, bạn có thể triển khai logic tùy chỉnh cả trước lẫn sau khi route handler cuối cùng được thực thi. Rõ ràng bạn có thể viết code trong phương thức intercept() chạy trước khi gọi handle(), nhưng làm sao để tác động đến những gì xảy ra sau đó? Vì phương thức handle() trả về một Observable, chúng ta có thể dùng các toán tử RxJS mạnh mẽ để xử lý thêm response. Theo thuật ngữ Aspect Oriented Programming, việc gọi route handler (tức là gọi handle()) được gọi là Pointcut, cho biết đó là điểm mà logic bổ sung của chúng ta được chèn vào.
Xét ví dụ, một request POST /cats đến. Request này được định tuyến đến handler create() được định nghĩa trong CatsController. Nếu có một interceptor không gọi phương thức handle() nằm ở đâu đó trên đường đi, method create() sẽ không được thực thi. Một khi handle() được gọi (và Observable của nó được trả về), handler create() sẽ được kích hoạt. Và một khi response stream nhận được qua Observable, các thao tác bổ sung có thể được thực hiện trên stream đó, và kết quả cuối cùng được trả về cho bên gọi.
Ghi chú người dịch Nếu bạn từng viết middleware kiểu func(next http.Handler) http.Handler ở Go, hoặc middleware Express dạng (req, res, next) => {...}, ý tưởng “bọc” hàm gốc để chạy code trước/sau ở đây là tương tự. Khác biệt là interceptor Nest không gọi next() đồng bộ mà trả về một Observable, nên “phần sau” được xử lý bằng operator RxJS (tap, map…) thay vì viết code ngay sau lệnh gọi handler.
Aspect interception
Use case đầu tiên chúng ta sẽ xem xét là dùng interceptor để log tương tác của user (ví dụ: lưu lại các lần gọi của user, dispatch event bất đồng bộ hoặc tính toán timestamp). Dưới đây là một LoggingInterceptor đơn giản:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@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`)),
);
}
}
import { Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor {
intercept(context, next) {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
Gợi ý NestInterceptor<T, R> là một generic interface, trong đó T biểu thị kiểu của Observable<T> (đại diện cho response stream), còn R là kiểu của giá trị được bọc bởi Observable<R>.
Lưu ý Interceptor, giống như controller, provider, guard, v.v., có thể inject dependency thông qua constructor.
Vì handle() trả về một Observable của RxJS, chúng ta có rất nhiều toán tử (operator) để xử lý stream này. Trong ví dụ trên, chúng ta dùng toán tử tap(), gọi hàm log ẩn danh của chúng ta khi observable stream kết thúc (dù thành công hay có lỗi), mà không can thiệp vào chu trình response.
Ghi chú người dịch Nếu bạn quen Promise hơn RxJS: tap(fn) giống .then(v => { log(v); return v; }) — chạy side-effect khi có giá trị trả về mà không làm thay đổi giá trị đó. (Muốn chạy cả khi thành công lẫn lỗi như .finally() thì RxJS có operator finalize().) Cạm bẫy hay gặp: quên return kết quả của next.handle().pipe(...) trong intercept() — khi đó Nest không nhận được Observable để tiếp tục pipeline và request sẽ lỗi thay vì trả response bình thường.
Gắn (bind) interceptor
Để thiết lập interceptor, chúng ta dùng decorator @UseInterceptors() được import từ package @nestjs/common. Giống như pipe và guard, interceptor có thể ở phạm vi controller, phạm vi method, hoặc phạm vi global.
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
Gợi ý Decorator @UseInterceptors() được import từ package @nestjs/common.
Với cách viết trên, mỗi route handler được định nghĩa trong CatsController sẽ sử dụng LoggingInterceptor. Khi ai đó gọi endpoint GET /cats, bạn sẽ thấy output sau trên standard output:
Before...
After... 1ms
Lưu ý rằng chúng ta truyền vào class LoggingInterceptor (thay vì một instance), để việc khởi tạo được giao cho framework xử lý, qua đó cho phép dependency injection. Cũng như pipe, guard và exception filter, chúng ta cũng có thể truyền vào một instance được tạo sẵn:
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}
Như đã đề cập, cách viết trên gắn interceptor vào mọi handler được khai báo trong controller này. Nếu muốn giới hạn phạm vi interceptor cho một method duy nhất, chúng ta chỉ cần áp dụng decorator ở cấp method.
Để thiết lập một interceptor toàn cục (global), chúng ta dùng phương thức useGlobalInterceptors() của instance ứng dụng Nest:
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
Global interceptor được sử dụng cho toàn bộ ứng dụng, áp dụng cho mọi controller và mọi route handler. Về mặt dependency injection, các global interceptor đăng ký từ bên ngoài bất kỳ module nào (bằng useGlobalInterceptors(), như ví dụ trên) không thể inject dependency, vì việc này được thực hiện ngoài context của bất kỳ module nào. Để giải quyết vấn đề này, bạn có thể thiết lập interceptor trực tiếp từ một module bất kỳ theo cách sau:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
Gợi ý Khi dùng cách này để thực hiện dependency injection cho interceptor, lưu ý rằng bất kể
module nào được dùng để khai báo, interceptor này thực chất vẫn là interceptor toàn cục. Nên đặt ở đâu? Hãy chọn module
nơi interceptor (LoggingInterceptor trong ví dụ trên) được định nghĩa. Ngoài ra, useClass không phải cách duy nhất để đăng ký custom provider. Tìm hiểu thêm tại đây.
Ánh xạ response (Response mapping)
Chúng ta đã biết handle() trả về một Observable. Stream này chứa giá trị được trả về từ route handler, do đó chúng ta có thể dễ dàng biến đổi nó bằng toán tử map() của RxJS.
Cảnh báo Tính năng response mapping không hoạt động với chiến lược response đặc thù của thư viện (việc dùng trực tiếp object @Res() là bị cấm).
Ghi chú người dịch Lý do là khi bạn tự inject @Res() và gọi res.send()/res.json() trực tiếp (giống viết Express thuần), bạn đã tự tay kết thúc response ngoài luồng mà Nest quản lý, nên Observable mà interceptor xử lý sau đó không còn tác dụng để map hay transform nữa.
Hãy tạo TransformInterceptor, sẽ chỉnh sửa mỗi response theo cách đơn giản để minh họa quy trình. Nó sẽ dùng toán tử map() của RxJS để gán response object vào thuộc tính data của một object mới được tạo ra, rồi trả object mới đó về cho client.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => ({ data })));
}
}
import { Injectable } from '@nestjs/common';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor {
intercept(context, next) {
return next.handle().pipe(map(data => ({ data })));
}
}
Gợi ý Interceptor của Nest hoạt động với cả phương thức intercept() đồng bộ lẫn bất đồng bộ. Bạn có thể đơn giản chuyển phương thức thành async nếu cần.
Với cách viết trên, khi ai đó gọi endpoint GET /cats, response sẽ trông như sau (giả sử route handler trả về một mảng rỗng []):
{
"data": []
}
Interceptor mang lại giá trị lớn trong việc tạo ra các giải pháp tái sử dụng được cho những yêu cầu xuất hiện xuyên suốt cả ứng dụng.
Ví dụ, giả sử chúng ta cần chuyển đổi mỗi giá trị null thành một chuỗi rỗng ''. Chúng ta có thể làm việc này chỉ với một dòng code và gắn interceptor toàn cục để nó tự động được dùng bởi mọi handler đã đăng ký.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(map(value => value === null ? '' : value ));
}
}
import { Injectable } from '@nestjs/common';
import { map } from 'rxjs/operators';
@Injectable()
export class ExcludeNullInterceptor {
intercept(context, next) {
return next
.handle()
.pipe(map(value => value === null ? '' : value ));
}
}
Ánh xạ exception (Exception mapping)
Một use case thú vị khác là tận dụng toán tử catchError() của RxJS để ghi đè exception được throw ra:
import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(() => new BadGatewayException())),
);
}
}
import { Injectable, BadGatewayException } from '@nestjs/common';
import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorsInterceptor {
intercept(context, next) {
return next
.handle()
.pipe(
catchError(err => throwError(() => new BadGatewayException())),
);
}
}
Ghi đè stream (Stream overriding)
Có một số lý do khiến đôi khi chúng ta muốn hoàn toàn ngăn việc gọi handler và trả về một giá trị khác thay thế. Một ví dụ rõ ràng là triển khai cache để cải thiện thời gian response. Hãy xem qua một cache interceptor đơn giản, trả về response từ cache. Trong một ví dụ thực tế, chúng ta sẽ cần cân nhắc thêm các yếu tố khác như TTL, invalidate cache, kích thước cache, v.v., nhưng điều đó nằm ngoài phạm vi bài viết này. Ở đây chúng ta chỉ đưa ra một ví dụ cơ bản để minh họa ý tưởng chính.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}
import { Injectable } from '@nestjs/common';
import { of } from 'rxjs';
@Injectable()
export class CacheInterceptor {
intercept(context, next) {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}
CacheInterceptor của chúng ta có một biến isCached được hardcode, và một response hardcode []. Điểm mấu chốt cần lưu ý là chúng ta trả về một stream mới ở đây, được tạo bởi toán tử of() của RxJS, do đó route handler sẽ không được gọi chút nào. Khi ai đó gọi một endpoint có sử dụng CacheInterceptor, response (một mảng rỗng được hardcode) sẽ được trả về ngay lập tức. Để tạo một giải pháp tổng quát, bạn có thể tận dụng Reflector và tạo một decorator tùy chỉnh. Reflector được mô tả kỹ trong chương guard.
Nhiều toán tử hơn (More operators)
Khả năng xử lý stream bằng các toán tử RxJS mang lại cho chúng ta rất nhiều khả năng. Hãy xem xét một use case phổ biến khác. Giả sử bạn muốn xử lý timeout cho các request đến route. Khi endpoint của bạn không trả về gì sau một khoảng thời gian, bạn muốn dừng lại với một response lỗi. Cách viết sau đây cho phép làm điều đó:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
};
};
import { Injectable, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor {
intercept(context, next) {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
};
};
Sau 5 giây, quá trình xử lý request sẽ bị hủy. Bạn cũng có thể thêm logic tùy chỉnh trước khi throw RequestTimeoutException (ví dụ: giải phóng tài nguyên).