上节我们介绍了在Nest.js中如何使用TypeORM进行数据库的操作,也写了几个简单的接口,但是接口的返回值没有任何的格式,但是在实际的开发中,我们需要将接口返回值进行格式化,比如返回值中需要包含状态码、消息、数据等,这就需要我们使用到Nest.js中的拦截器了,但是在介绍拦截器之前,我们先来了解一下Nest.js中的中间件和管道。
中间件
中间件是在路由处理器之前 调用的函数,可以访问请求 和响应 的对象,以及应用程序的请求-响应周期中的 next()
中间件函数。默认情况下,Nest的中间件等同于Express的中间件。
中间件函数可以执行以下任务:
- 执行任何代码
- 对请求和响应对象进行更改
- 结束请求-响应周期
- 调用堆栈中的下一个中间件函数
- 如果当前中间件函数没有结束请求-响应周期,则必须调用
next()
将控制权传递给下一个中间件函数,否则,请求将被挂起。
一般情况下我们会在中间件中处理一些公共的逻辑,比如日志记录、身份验证、请求拦截/限制、响应拦截、跨域处理等。
那怎么实现中间件呢?在Nest.js中,我们可以通过实现 NestMiddleware
接口来创建中间件,然后使用 @Injectable()
装饰器将其标记为提供者。
ts
import { NestMiddleware } from "@nestjs/common";
import { NextFunction, Request, Response } from "express";
export class TestMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log("Test Middleware", req.baseUrl, req.method);
next();
}
}
中间件可以在模块级别或全局级别注册。先说某个模块上注册,我们将这个中间件注册到 app.module
模块中
ts
import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common";
import { TestMiddleware } from "./test.middleware";
@Module()
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TestMiddleware).forRoutes('test');
}
}
这样我们就创建了一个中间件,并将其注册到了 test
路由上,当访问 test
路由时,中间件就会被触发。
我们使用 nest g resource test
生成一个模块,并使用RERSTful API生成一些CURD示例,这时候我们访问 http://localhost:3000/test
看一下

我们也可以访问上节的一些接口,比如 http://localhost:3000/user
,看一下中间件是否生效
这个forRoutes方法还可以接受一个数组,用于指定多个路由,比如 forRoutes('test', 'user')
,这样中间件就会在 test
和 user
路由上生效,也支持使用通配符,比如 forRoutes('*')
、forRoutes('user/*')
,也可以指定请求方法。
ts
forRoutes({
path: 'user/*',
method: RequestMethod.GET, // 指定路由 user开头的所有GET请求
});
也可以使用exclude方法排除一些路由,比如 exclude('user')
,这样中间件就不会在 user
路由上生效。
ts
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TestMiddleware).forRoutes('*').exclude('user');
}
}
当有多个中间件时,可以使用 apply()
方法将它们组合在一起,比如 consumer.apply(TestMiddleware1, TestMiddleware2).forRoutes('test')
,这样中间件就会按照顺序依次执行。
那如果我们想注册到全局的话,可以在 main.ts
中使用 app.use()
方法,比如 app.use(TestMiddleware)
,这样中间件就会在所有路由上生效。
ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
stopAtFirstError: true,
}),
);
app.use(TestMiddleware);
await app.listen(process.env.PORT ?? 3000);
}
管道
管道是专门用于数据转换 和数据验证的类,它可以在数据从进入控制器之前或从控制器返回之后进行处理。管道可以用于以下场景:
参数验证
:可结合class-validator
库使用,对请求参数进行验证,比如必填项、类型、长度等。数据转换
:将请求参数转换为指定的类型,比如将字符串转换为数字、将日期字符串转换为日期对象等。数据清洗
:对请求参数进行清洗,比如去除空格、去除特殊字符或者过滤敏感字段等。
Nest.js内置了几个常用的管道,比如 ValidationPipe
、ParseIntPipe
、ParseFloatPipe
、ParseBoolPipe
、ParseArrayPipe
、ParseUUIDPipe
、ValidationPipe
等,我们也可以自定义管道。
我们前面在 main.ts
中使用了 ValidationPipe
,这个管道就是用于参数验证的,配合 class-validator
做数据验证,也可以使用 class-transformer
做数据转换。
那自定义管道怎么实现呢?在Nest.js中,我们可以通过实现 PipeTransform
接口来创建管道,并通过 transform
方法处理数据,然后使用 @Injectable()
装饰器将其标记为提供者。
ts
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from "@nestjs/common";
@Injectable()
export class TestPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
const val = parseInt(value, 16); // 按16进制解析字符串
if (isNaN(val)) {
throw new BadRequestException('参数类型错误');
} else {
return val;
}
}
}
transform
有两个参数,第一个参数 value
是当前处理的方法参数(在路由处理方法接收到它之前),第二个参数 metadata
是当前处理的方法参数的元数据,元数据中包含了请求参数的类型、请求参数的名称等信息,类型如下:
ts
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
我们该怎么去用呢?我们打开 test.ontroller.ts 在里面引入我们的TestPipe,并且写一个新的方法,使用TestPipe对参数进行验证,代码如下:
ts
@Get(':id')
findTargetOne(@Param('id', new TestPipe()) id: number) {
return this.testService.findOne(id);
}
我们service中的findone方法没变,还是这个
ts
findOne(id: number) {
return `This action returns a #${id} test`;
}
这时候我们访问一下 http://localhost:3000/test/12.235 发送一个get请求,发现返回如下:

可以看到,我们传的参数是 12.235
,但是我们的管道将参数转换为十六进制的整数类型,所以返回的结果是 This action returns a #18 test
。
这是局部使用管道,我们也可以全局使用管道,在 main.ts
中使用 app.useGlobalPipes()
方法,比如 app.useGlobalPipes(new ValidationPipe())
,这样管道就会在所有路由上生效。
拦截器
拦截器是Nest.js中用于全局处理请求和响应的类,它可以在请求进入控制器之前或控制器返回响应之后进行处理。拦截器可以用于以下场景:
- 认证和授权
- 日志记录
- 异常处理
- 请求和响应的转换
- 请求和响应的过滤
Nest.js内置了几个常用的拦截器,比如 LoggerInterceptor
、TimeoutInterceptor
、CacheInterceptor
、SerializeInterceptor
、CatchInterceptor
等,我们也可以自定义拦截器。
那我们怎么实现拦截器呢?在Nest.js中,我们可以通过实现 NestInterceptor
接口来创建拦截器,并通过 intercept()
方法处理请求和响应,然后使用 @Injectable()
装饰器将其标记为提供者。
拦截器需要实现 NestInterceptor
接口的 intercept()
方法,该方法接收两个参数:
context
: ExecutionContext:包含当前请求的上下文信息(如请求对象、控制器、方法等)。next
: CallHandler:用于获取控制器方法返回的 observable(表示方法的执行结果)。
我们可以在这个方法中响应进行处理,比如:
ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
// 定义统一响应结构
interface Response<T> {
code: number;
message: string;
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) => {
return {
code: 200,
message: 'success',
data: data || null,
};
})
)
}
}
看这段代码似乎有点难懂,那我们先了解一下 rxjs
的概念。
rxjs
是一个用于处理异步数据流的库,它提供了一种可观察的对象(observable)来表示异步数据流,并且可以使用操作符(operator)对数据流进行处理。
在 Nest.js 中,控制器方法的返回值就是一个 observable,我们可以使用 pipe()
方法对它进行处理,比如:
ts
@Get()
findAll(): Observable<any> {
return this.testService.findAll();
}
我们可以在 findAll()
方法中使用 pipe()
方法对返回的 observable 进行处理,比如:
ts
@Get()
findAll(): Observable<any> {
return this.testService.findAll().pipe(map((data) => {
return {
code: 200,
message: 'success',
data: data || null,
};
}));
}
那 map 是做什么的,除了 map 还有哪些方法呢?我们这里列举一些常用的方法:
map()
:接收 Observable 流中的每一个数据,对其进行转换处理,并返回一个新的 Observable 流(包含转换后的数据)。tap()
: 执行副作用操作(如日志打印、计时、缓存存储),但不修改流中的数据,也不影响流的传递,可理解为"不修改数据,只做额外操作"。filter()
:过滤 observable 的数据,只保留满足条件的值,比如做数据筛选(如只对 GET 请求的返回结果做缓存,POST 请求跳过)。take()
:只取 Observable 流中的前 N 个数据,之后自动完成流(忽略后续数据),限制返回数据量(如缓存拦截中,只保留最新的 1 条缓存数据)。retry
: 当 Observable 流出错时,自动重试指定次数(若重试成功则继续,失败则抛出错误),临时故障恢复(如数据库连接偶发失败时,重试 2 次后再返回错误)。catchError()
:捕获 observable 的错误,并返回一个新的 observable,比如自定义异常处理(如将业务错误转换为标准错误响应,替代默认的 500 页面)。finalize()
:无论 Observable 流是 "成功完成" 还是 "出错终止",都会执行的逻辑(类似 finally),可以做资源释放(如请求结束后关闭数据库连接、清除临时变量)。
回头我们再理解上面的,在 intercept()
方法中,我们使用 next.handle()
获取控制器方法返回的 observable,然后使用 map()
方法将 observable 转换为一个新的 observable,新的 observable 的数据格式为 { code: 200, message: 'success', data: data || null }
。
我们将这个拦截器添加到全局,在 main.ts
中使用 app.useGlobalInterceptors()
方法,比如 app.useGlobalInterceptors(new TransformInterceptor())
,这样拦截器就会在所有路由上生效。
ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { TransformInterceptor } from './common/transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 自动移除未在 DTO 中定义的字段
forbidNonWhitelisted: true, // 对未定义的字段抛出错误
transform: true, // 自动将请求数据转换为 DTO 实例
stopAtFirstError: true, // 在第一个验证错误时停止验证
}),
);
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
这时候我们访问上面我们写过的接口 http://localhost:3000/test/12.235
,看一下返回结果

当然我们也是可以局部使用拦截器的,在控制器或者模块中使用 @UseInterceptors()
装饰器,比如在我们的test.controller中使用
ts
@Get()
findAll() {
return this.testService.findAll();
}
@Get(':id')
@UseInterceptors(TransformInterceptor)
findOne(@Param('id', new TestPipe()) id: number) {
return this.testService.findOne(id);
}
我们分别访问 http://localhost:3000/test
和 http://localhost:3000/test/12.235
,看一下返回结果发现只有第二个接口返回的是我们重新定义的格式,第一个接口返回的还是原来的格式。
有时候我们想知道一个接口执行的时长,以监测某个接口是否存在性能问题,这时候我们可以使用拦截器来实现 monitor.interceptor.ts
ts
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from "@nestjs/common";
import { Observable, tap } from "rxjs";
@Injectable()
export class MonitorInterceptor implements NestInterceptor {
private logger = new Logger('monitor')
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
const req = context.switchToHttp().getRequest();
return next.handle().pipe(
tap(() => {
const time = Date.now() - start;
this.logger.log(`Request ${req.method} ${req.url} took ${time}ms`);
})
)
}
}
然后我们在全局引入这个拦截器
ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { TransformInterceptor } from './common/transform.interceptor';
import { MonitorInterceptor } from './common/monitor.interceptor'
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 自动移除未在 DTO 中定义的字段
forbidNonWhitelisted: true, // 对未定义的字段抛出错误
transform: true, // 自动将请求数据转换为 DTO 实例
stopAtFirstError: true, // 在第一个验证错误时停止验证
}),
);
app.useGlobalInterceptors(new TransformInterceptor());
app.useGlobalInterceptors(new MonitorInterceptor());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
这时候我们随便请求我们的一个接口,比如还是这个 http://localhost:3000/test/12.235
,看一下控制台输出

可以看到我们请求的接口执行了 1ms。
三者区别
特性 | 中间件(Middleware) | 管道(Pipes) | 拦截器(Interceptors) |
---|---|---|---|
核心功能 | 预处理请求 / 后处理响应(通用逻辑) | 数据验证与转换(输入数据处理) | 增强方法执行、拦截返回值 / 异常 |
执行时机 | 请求进入 → 路由匹配后,管道之前 | 中间件之后,控制器方法执行前 | 管道之后,控制器方法执行前后 |
处理对象 | req/res 对象(请求上下文) | 控制器方法的输入参数 | 控制器方法的执行过程与返回值 |
核心 API | 实现 NestMiddleware 接口的 use() | 实现 PipeTransform 接口的 transform() | 实现 NestInterceptor 接口的 intercept() |
典型用途 | 日志、认证、限流、跨域 | 参数校验、类型转换、数据清洗 | 响应格式化、缓存、异常捕获、性能监控 |
一个完整的请求生命周期中这三者的执行顺序是:
客户端请求 → 中间件(预处理) → 管道(验证/转换参数) → 控制器方法执行 → 拦截器(处理返回值) → 中间件(后处理,如记录耗时) → 响应客户端