内置 Pipe 与自定义 Pipe:参数校验和转换的利器


内置 Pipe 与自定义 Pipe:参数校验和转换的利器

大家好,今天我们来聊聊 NestJS 里一个非常实用但又容易被低估的组件——管道(Pipe)。如果你写过 Controller,一定见过 @Body() createUserDto: CreateUserDto 这种写法,配合 ValidationPipe 就能自动验证参数格式。这就是管道最典型的应用场景。管道主要有两个作用:参数校验参数转换。它可以在请求到达控制器方法之前,对参数进行预处理,如果不符合要求就直接抛出异常,避免脏数据进入业务逻辑。Nest 内置了很多好用的管道,同时也允许我们自定义管道,今天我们就来一起看一下。


一、内置管道:开箱即用

NestJS 提供了一系列内置管道,覆盖了大部分常见需求。我们简单过一遍:

  • ValidationPipe:最强大的管道,基于 class-validatorclass-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 能极大简化参数校验代码。而当内置管道不能满足需求时,自定义管道给了我们充分的灵活性,甚至可以通过注入依赖实现复杂的校验逻辑。记住,管道可以在参数级别、方法级别、控制器级别甚至全局使用,灵活组合能让你的代码既简洁又健壮。

声明:麋鹿与鲸鱼|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 内置 Pipe 与自定义 Pipe:参数校验和转换的利器


Carpe Diem and Do what I like