首页 / NestJS 入门教程 / 拦截器 Interceptor

NestJS 入门教程

拦截器 Interceptor

本教程共 47 篇 · 第 18 篇 · 更新于 2026-08-09 · 约 13 分钟阅读

NestJSInterceptor拦截器AOPRxJS响应转换日志

本节目标:搞懂拦截器的执行机制,学会用拦截器做响应转换、日志记录、异常处理、超时控制和缓存。

拦截器是什么

如果说守卫是小区的”门禁”,那拦截器更像是快递柜。

快递柜能在两个时间点介入:快递员存件的时候(请求进入控制器之前)和用户取件的时候(响应返回给客户端之前)。你可以在存件时做检查、在取件时做包装,整个过程就像”夹心饼干”一样把控制器方法包在中间。

用代码来说,拦截器是一个用 @Injectable() 装饰的类,它实现了 NestInterceptor 接口:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class MyInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // 控制器方法执行前的逻辑

    return next.handle().pipe(
      // 控制器方法执行后的逻辑
    );
  }
}

两个参数:

  • ExecutionContext:和守卫里的一模一样,能拿到请求、控制器、方法等所有信息
  • CallHandler:它的 handle() 方法返回一个 Observable,代表控制器方法的执行流
Note

如果你不调用 next.handle(),控制器方法就不会执行。这给了你完全的控制权——可以根据条件直接返回缓存数据,跳过控制器。

执行流程

拦截器横跨控制器方法的”前”和”后”两个阶段:

请求 → 守卫 → 拦截器(前) → 管道 → 控制器方法 → 拦截器(后) → 响应

intercept() 方法里,调用 next.handle() 之前的代码在控制器执行前运行,之后的代码在控制器执行后运行。

拦截器能干什么

官方文档列了五种典型用途:

  1. 方法执行前后添加额外逻辑(比如日志、计时)
  2. 转换返回值(比如统一响应格式)
  3. 转换异常(比如把底层异常映射成友好的错误信息)
  4. 扩展方法行为(比如给响应加额外字段)
  5. 完全覆盖方法(比如缓存命中时直接返回,不执行控制器)

这其实就是 AOP(面向切面编程)的思想——把通用逻辑从业务代码中抽出来,集中管理。

写一个日志拦截器

最经典的入门例子:记录每个请求的耗时。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

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

    this.logger.log(`请求进入: ${method} ${url}`);

    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - now;
        this.logger.log(`请求完成: ${method} ${url} 耗时 ${duration}ms`);
      }),
    );
  }
}

tap 是 RxJS 的操作符,它能在数据流经过时”偷看”一下,做一些副作用操作(比如打印日志),但不会改变数据本身。

Tip

tap 特别适合做日志、监控这类”只看不改”的操作。数据经过 tap 之后原封不动地继续往下走。

响应转换拦截器

这个在实际项目中用得非常多。后端返回的数据格式经常需要统一包装,比如包一层 { code, message, data }

如果每个控制器方法都手动包装,代码会很冗余。用拦截器就能一处定义、全局生效。

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;
  timestamp: string;
}

@Injectable()
export class TransformInterceptor<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,
        timestamp: new Date().toISOString(),
      })),
    );
  }
}

控制器方法只需要返回原始数据:

@Get()
findAll() {
  return [{ id: 1, name: '张三' }];
}

客户端收到的响应会自动变成:

{
  "code": 200,
  "message": "success",
  "data": [{ "id": 1, "name": "张三" }],
  "timestamp": "2026-08-09T00:00:00.000Z"
}
Warning

响应转换拦截器不能和 @Res() 一起用。一旦你用了 @Res() 直接操作响应对象,NestJS 就失去了对响应的控制权,拦截器的 map 操作会失效。

异常映射拦截器

有时候底层抛出的异常不够友好,你想统一转换一下。拦截器里的 catchError 操作符就是干这个的。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, NotFoundException } 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 => {
        // 把数据库的"记录不存在"错误转换成 404
        if (error.code === 'P2025') {
          return throwError(() => new NotFoundException('记录不存在'));
        }
        // 其他错误原样抛出
        return throwError(() => error);
      }),
    );
  }
}
Note

拦截器里抛出的异常和控制器里抛出的异常一样,会被 NestJS 的异常过滤器统一处理。你不用担心异常”漏出去”。

超时拦截器

接口响应太慢?加个超时保护。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { timeout, catchError } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(5000),  // 5 秒超时
      catchError(err => {
        if (err instanceof TimeoutError) {
          return throwError(() => new RequestTimeoutException('请求超时,请稍后重试'));
        }
        return throwError(() => err);
      }),
    );
  }
}

timeout(5000) 是 RxJS 的操作符,如果 5 秒内数据流没有返回数据,就会抛出一个 TimeoutError。拦截器捕获它,转换成 RequestTimeoutException(HTTP 408)。

缓存拦截器

拦截器还有一个”大招”——完全跳过控制器方法,直接返回数据。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

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

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const key = `${request.method}:${request.url}`;

    // 缓存命中 → 直接返回,控制器不会执行
    if (this.cache.has(key)) {
      return of(this.cache.get(key));
    }

    // 缓存未命中 → 正常执行控制器,并缓存结果
    return next.handle().pipe(
      tap(data => {
        this.cache.set(key, data);
      }),
    );
  }
}

关键点在于 of() 这个 RxJS 操作符。它把一个普通值包装成 Observable 返回。当你返回 of(cachedData) 时,next.handle() 根本没被调用,控制器方法自然就不会执行。

Tip

这个例子只是演示原理。实际项目中建议用 @nestjs/cache-manager 提供的缓存方案,它支持 TTL、缓存失效、多种存储后端等高级特性。

拦截器的三种绑定方式

和守卫完全一致,支持方法级、控制器级、全局级。

方法级别

@Controller('users')
export class UsersController {
  @Get()
  @UseInterceptors(LoggingInterceptor)
  findAll() {
    return [];
  }
}

控制器级别

@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
  // 所有方法都会被 LoggingInterceptor 拦截
}

全局级别

// 方式一:在 main.ts 中注册(无法注入依赖)
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());

// 方式二:在模块中注册(推荐,支持依赖注入)
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

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

和守卫一样,全局拦截器用 APP_INTERCEPTOR 注册才能注入依赖。

多个拦截器的执行顺序

你可以同时挂多个拦截器:

@UseInterceptors(LoggingInterceptor, TransformInterceptor)

执行顺序像”洋葱模型”——先进后出:

LoggingInterceptor(前) → TransformInterceptor(前) →
  控制器方法
→ TransformInterceptor(后) → LoggingInterceptor(后)

第一个拦截器的”前”逻辑最先执行,它的”后”逻辑最后执行。

Note

这个顺序很重要。比如日志拦截器应该放在最外层(数组第一个),这样它才能记录包括其他拦截器在内的完整耗时。

性能监控拦截器

把日志拦截器稍微改一下,就能做一个慢请求监控:

@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const startTime = Date.now();
    const request = context.switchToHttp().getRequest();

    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - startTime;
        const { method, url } = request;

        if (duration > 1000) {
          console.warn(`慢请求警告: ${method} ${url} 耗时 ${duration}ms`);
        }
      }),
    );
  }
}

超过 1 秒的请求会被标记出来。生产环境里可以把阈值调低一些,或者把告警接入监控系统。

拦截器最佳实践

1. 单一职责

每个拦截器只做一件事。日志归日志,转换归转换,缓存归缓存。

// 好的做法
@UseInterceptors(LoggingInterceptor, TransformInterceptor, CacheInterceptor)

// 不好的做法:一个拦截器干所有事
@UseInterceptors(DoEverythingInterceptor)

2. 用装饰器传递配置

不要在拦截器里硬编码,用自定义装饰器把配置贴在路由上:

import { SetMetadata } from '@nestjs/common';

export const CACHE_TTL_KEY = 'cache_ttl';
export const CacheTTL = (seconds: number) => SetMetadata(CACHE_TTL_KEY, seconds);

@Get()
@CacheTTL(120)  // 缓存 120 秒
findAll() {}

3. 善用 RxJS 操作符

拦截器的威力来自 RxJS。常用的几个操作符:

  • tap:做副作用,不改数据(日志、计时)
  • map:转换数据(响应格式化)
  • catchError:捕获异常(异常映射)
  • timeout:超时控制
  • filter:过滤数据

4. 拦截器和 @Res() 不兼容

用了 @Res() 就拿不到标准的响应流了,拦截器的 map 等操作符会失效。如果某个方法需要 @Res(),就别给它挂响应转换拦截器。

小结

拦截器的核心价值是”在控制器方法的前后插入通用逻辑”。

关键知识点回顾:

  • 拦截器实现 NestInterceptor 接口,核心方法是 intercept()
  • next.handle() 返回 Observable,代表控制器的执行流
  • 不调用 next.handle() 可以跳过控制器(缓存场景)
  • map 转换响应、catchError 转换异常、tap 做副作用
  • 三种绑定方式:方法级、控制器级、全局级
  • 全局拦截器用 APP_INTERCEPTOR 注册才能注入依赖
  • 多个拦截器按”洋葱模型”执行,先进后出

下一章我们学自定义装饰器,让代码写起来更简洁。