内置 Pipe 与自定义 Pipe:参数校验和转换的利器
大家好,今天我们来聊聊 NestJS 里一个非常实用但又容易被低估的组件——管道(Pipe)。如果你写过 Controller,一定见过 @Body() createUserDto: CreateUserDto 这种写法,配合 ValidationPipe 就能自动验证参数格式。这就是管道最典型的应用场景。管道主要有两个作用:参数校验和参数转换。它可以在请求到达控制器方法之前,对参数进行预处理,如果不符合要求就直接抛出异常,避免脏数据进入业务逻辑。Nest 内置了很多好用的管道,同时也允许我们自定义管道,今天我们就来一起看一下。
一、内置管道:开箱即用
NestJS 提供了一系列内置管道,覆盖了大部分常见需求。我们简单过一遍:
- ValidationPipe:最强大的管道,基于
class-validator和class-transformer,用于验证 DTO 对象。 - ParseIntPipe:将字符串参数转换为整数,如果转换失败抛出异常。
- ParseBoolPipe:将字符串转换为布尔值(比如 'true' 转成 true)。
- ParseArrayPipe:将字符串解析为数组,常用于 query 参数传递数组的场景。
- ParseUUIDPipe:验证参数是否为有效的 UUID。
- DefaultValuePipe:当参数未提供时,设置一个默认值。
- ParseEnumPipe:验证参数是否属于某个枚举值。
- ParseFloatPipe:转换为浮点数。
- ParseFilePipe:验证上传的文件。
这些管道可以直接用在参数上,比如:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// id 已经是 number 类型
}如果参数转换失败,Nest 会自动返回 400 错误,非常省心。
二、ValidationPipe:最常用的校验管道
在实际开发中,最常用的当属 ValidationPipe。它通常配合 DTO(数据传输对象)使用,让你通过装饰器声明校验规则。
首先需要安装两个依赖包:
npm install class-validator class-transformer然后定义一个 DTO 类,用 class-validator 的装饰器标记规则:
import { IsString, IsInt, Min, Max } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsInt()
@Min(0)
@Max(150)
age: number;
}接着在控制器中使用:
@Post()
create(@Body() createUserDto: CreateUserDto) {
// 如果参数不符合规则,ValidationPipe 会自动抛出 BadRequestException
return createUserDto;
}为了启用 ValidationPipe,你需要把它应用到全局或局部。最简单的就是在 main.ts 中设置全局管道:
app.useGlobalPipes(new ValidationPipe());ValidationPipe 的工作原理
文件里提到了它的原理:基于 class-transformer 把普通对象转换成 DTO 类的实例,然后基于 class-validator 的装饰器对这个实例进行验证。如果验证失败,就抛出异常。
也就是说,Nest 会自动把请求体对象(比如 { name: 'John', age: 30 })转换成 CreateUserDto 的实例,然后检查 name 是不是字符串、age 是不是整数且在 0~150 之间。验证不通过就会返回详细的错误信息。
三、自定义管道:自己的需求自己造
虽然内置管道已经很丰富了,但总有特殊需求需要自己动手。比如你想把参数值乘以 10 再传给控制器,或者做更复杂的校验。
自定义管道需要实现 PipeTransform 接口,也就是写一个类,实现 transform 方法。这个方法接收两个参数:
value:当前参数的值(来自请求)metadata:包含该参数的元数据,比如参数名、类型、装饰器类型等
transform 方法的返回值就是最终传给控制器方法的值。如果校验失败,可以直接抛出异常(比如 BadRequestException)。
例子:自定义一个 ParseIntPipe
其实 Nest 已经内置了 ParseIntPipe,但我们自己实现一个来理解流程:
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class MyParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('参数必须为数字');
}
return val;
}
}使用:
@Get(':id')
findOne(@Param('id', MyParseIntPipe) id: number) {}例子:更复杂的校验管道
假设我们需要一个管道,它根据参数元数据来动态校验。比如我们想实现一个类似 ValidationPipe 但只对特定字段生效的管道。实际上,Nest 的 ArgumentMetadata 包含了 type(是 body、query 还是 param)、metatype(参数的类型,比如 DTO 类)、data(装饰器传的值,比如 @Query('name') 的 'name')。我们可以利用这些信息做更精细的控制。
例如,我们想写一个管道,如果参数是数字字符串就转成数字,否则原样返回:
@Injectable()
export class ParseOptionalIntPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
if (value === undefined || value === null) return value;
const val = parseInt(value, 10);
return isNaN(val) ? value : val;
}
}依赖注入与全局管道
自定义管道也是 provider,所以它可以通过构造函数注入其他服务。比如我们想注入一个日志服务:
@Injectable()
export class CustomPipe implements PipeTransform {
constructor(private loggerService: LoggerService) {}
transform(value: any, metadata: ArgumentMetadata) {
this.loggerService.log('开始校验参数...');
// 校验逻辑
return value;
}
}但需要注意的是,如果你在 main.ts 中用 app.useGlobalPipes(new CustomPipe()) 注册全局管道,这个管道实例不在 IoC 容器中,无法注入依赖。解决办法是用 APP_PIPE 令牌在模块中注册:
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
LoggerService,
{
provide: APP_PIPE,
useClass: CustomPipe, // 这样 CustomPipe 就能注入 LoggerService 了
},
],
})
export class AppModule {}这样,CustomPipe 就会成为全局管道,并且享受依赖注入。
四、总结
管道是 NestJS 中实现参数预处理的核心机制。内置管道覆盖了大多数场景,尤其是 ValidationPipe,配合 DTO 和 class-validator 能极大简化参数校验代码。而当内置管道不能满足需求时,自定义管道给了我们充分的灵活性,甚至可以通过注入依赖实现复杂的校验逻辑。记住,管道可以在参数级别、方法级别、控制器级别甚至全局使用,灵活组合能让你的代码既简洁又健壮。

Comments | NOTHING