NestJS 拦截器(Interceptor)使用指南

NestJS 的 拦截器(Interceptor) 是一个强大的概念,它基于 面向切面编程(AOP) 思想,让你能够在方法执行前后插入自定义逻辑。

拦截器能做什么?

拦截器的功能非常丰富,常见的应用场景包括:

  • 在方法执行前后执行额外逻辑
  • 转换/映射函数的返回结果
  • 转换/覆盖函数抛出的异常
  • 扩展函数的基本行为
  • 完全覆盖函数的行为(比如缓存)

拦截器基础

每个拦截器都需要实现 NestInterceptor 接口,它要求实现 intercept 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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('进入拦截器...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`请求耗时: ${Date.now() - now}ms`)),
);
}
}

关键点:

  • ExecutionContext 与守卫中相同,提供当前请求的上下文
  • CallHandlerhandle() 方法返回一个 Observable,调用它才会执行路由处理器
  • 不调用 handle(),路由处理器就不会执行

使用拦截器

绑定到单个路由

1
2
3
4
5
6
7
8
9
10
11
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';

@Controller('users')
export class UsersController {
@Get('profile')
@UseInterceptors(LoggingInterceptor)
getProfile() {
return { name: 'Alice', age: 30 };
}
}

绑定到整个控制器

1
2
3
4
5
6
7
8
@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
@Get('profile')
getProfile() {
return 'profile';
}
}

全局拦截器

1
2
3
4
5
6
7
8
9
10
11
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
await app.listen(3000);
}
bootstrap();

通过模块注册全局拦截器(支持依赖注入):

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './logging.interceptor';

@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}

RxJS 操作符在拦截器中的应用

拦截器的核心在于 RxJS 的能力。以下是常用的操作符场景:

tap — 不修改响应,只做副作用

1
2
3
4
5
6
7
8
9
10
11
import { tap } from 'rxjs/operators';

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
tap({
next: (data) => console.log('响应数据:', data),
error: (err) => console.error('发生错误:', err),
complete: () => console.log('请求完成'),
}),
);
}

map — 转换响应数据

1
2
3
4
5
6
7
8
9
10
11
import { map } from 'rxjs/operators';

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => ({
code: 200,
message: 'success',
data,
})),
);
}

catchError — 捕获并处理异常

1
2
3
4
5
6
7
8
9
10
11
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((err) => {
console.error('拦截到错误:', err.message);
return throwError(() => err);
}),
);
}

timeout — 超时控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { timeout } from 'rxjs/operators';
import { RequestTimeoutException } from '@nestjs/common';

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError((err) => {
if (err.name === 'TimeoutError') {
return throwError(() => new RequestTimeoutException('请求超时'));
}
return throwError(() => err);
}),
);
}

实战案例

1. 统一响应格式

将所有接口的返回值包装成统一的 { code, message, data } 格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// response.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}

@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<ApiResponse<T>> {
return next.handle().pipe(
map((data) => ({
code: 200,
message: 'success',
data,
})),
);
}
}

全局注册后,控制器只需返回原始数据:

1
2
3
4
5
6
7
8
@Controller('users')
export class UsersController {
@Get('profile')
getProfile() {
return { name: 'Alice', age: 30 };
}
// 客户端收到的响应: { code: 200, message: "success", data: { name: "Alice", age: 30 } }
}

2. 请求日志拦截器

记录每个请求的详细信息,包括参数、耗时、响应状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// request-log.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class RequestLogInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, body, query } = request;
const now = Date.now();

return next.handle().pipe(
tap({
next: (data) => {
this.logger.log(
`${method} ${url} ${Date.now() - now}ms - 参数: ${JSON.stringify({ query, body })}`,
);
},
error: (error) => {
this.logger.error(
`${method} ${url} ${Date.now() - now}ms - 错误: ${error.message}`,
);
},
}),
);
}
}

3. 缓存拦截器

对 GET 请求的结果进行简单的内存缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
private readonly cache = new Map<string, any>();

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();

// 只缓存 GET 请求
if (request.method !== 'GET') {
return next.handle();
}

const cacheKey = request.url;
const cachedResponse = this.cache.get(cacheKey);

if (cachedResponse) {
return of(cachedResponse); // 直接返回缓存,不再调用路由处理器
}

return next.handle().pipe(
tap((data) => {
this.cache.set(cacheKey, data);
// 1分钟后自动清除缓存
setTimeout(() => this.cache.delete(cacheKey), 60000);
}),
);
}
}

4. 敏感字段过滤拦截器

自动移除响应中的敏感字段(如密码、token):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// sanitize.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class SanitizeInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => this.removeSensitiveFields(data)),
);
}

private removeSensitiveFields(data: any): any {
if (Array.isArray(data)) {
return data.map((item) => this.removeSensitiveFields(item));
}

if (data && typeof data === 'object') {
const sensitiveFields = ['password', 'token', 'secret', 'refreshToken'];
const cleaned = { ...data };
for (const field of sensitiveFields) {
delete cleaned[field];
}
return cleaned;
}

return data;
}
}

多个拦截器的执行顺序

多个拦截器的执行顺序遵循的原则:

1
2
3
4
5
6
7
8
9
@Controller('users')
@UseInterceptors(InterceptorA) // 外层
export class UsersController {
@Get('profile')
@UseInterceptors(InterceptorB) // 内层
getProfile() {
return 'profile';
}
}

执行顺序:

  1. InterceptorA.intercept() 的前置代码
  2. InterceptorB.intercept() 的前置代码
  3. 路由处理器 getProfile()
  4. InterceptorB.intercept() 的后置代码
  5. InterceptorA.intercept() 的后置代码

拦截器 vs 守卫 vs 管道 vs 中间件

组件 执行时机 主要用途
中间件 最先执行 日志、CORS、请求体解析
守卫 中间件之后 鉴权、权限校验
管道 守卫之后 参数校验、类型转换
拦截器 管道之后 响应映射、缓存、日志、异常处理

总结

NestJS 拦截器是最灵活的组件之一,它利用 RxJS 的强大能力让你能精细控制请求-响应周期:

  1. AOP 思想:在方法执行前后插入逻辑,实现关注点分离
  2. RxJS 驱动:借助 tapmapcatchErrortimeout 等操作符处理异步流
  3. 完全控制:可以选择不执行路由处理器(如缓存场景)
  4. 关注点分离:将统一响应格式、日志记录、缓存等横切关注点集中管理

结合之前介绍的中间件、守卫,加上拦截器,你就掌握了 NestJS 请求处理链路上的所有关卡,可以根据不同需求选择最合适的组件。