6月4日 15:41

NestJS管道和验证:class-validator配置、自定义Pipe和常见坑

NestJS 的管道(Pipe)就两件事:转换验证。转换是把字符串参数变成数字、把日期字符串变成 Date 对象;验证是检查请求体里的字段是否合法,不合法就拒绝请求。听起来简单,但管道是 NestJS 请求生命周期里的关键一环——守卫之后、控制器之前,数据必须过管道这一关。

管道的两种用途

  • 转换:把输入数据转成目标类型(如 ParseIntPipe 把路由参数 "42" 变成数字 42
  • 验证:检查输入数据是否合法,不合法抛异常(如 class-validator 检查 email 格式)

一个管道可以只做转换、只做验证,或者两者都做。NestJS 内置的管道偏向转换,实际项目里的验证管道通常结合 class-validator

内置管道

ParseIntPipe:路由参数转数字

typescript
@Get(':id') findOne(@Param('id', ParseIntPipe) id: number) { // 如果 id 不是数字,自动返回 400 Bad Request return this.usersService.findOne(id); }

不加 ParseIntPipe,id 是字符串 "42",你的 service 里拿到的类型和声明不一致。加了之后,不合法的值直接被拦截,控制器方法不会被调用。

ParseUUIDPipe

typescript
@Get(':id') findOne(@Param('id', new ParseUUIDPipe()) id: string) { return this.usersService.findOne(id); }

验证 UUID 格式。非法 UUID 返回 400,不需要自己写正则。

ParseArrayPipe

typescript
@Get() findAll(@Query('ids', new ParseArrayPipe({ items: String, separator: ',' })) ids: string[]) { // ?ids=a,b,c → ['a', 'b', 'c'] return this.usersService.findByIds(ids); }

DefaultValuePipe

typescript
@Get() findAll( @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, @Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number, ) { return this.usersService.findAll({ page, limit }); }

DefaultValuePipe 放在 ParseIntPipe 前面——先设默认值,再转数字。管道按参数顺序从左到右执行。

用 class-validator 做请求体验证

这是实际项目里用得最多的验证方式。

安装依赖

bash
npm install class-validator class-transformer

定义 DTO

typescript
import { IsString, IsEmail, IsInt, Min, IsOptional, IsEnum } from 'class-validator'; export class CreateUserDto { @IsString() name: string; @IsEmail() email: string; @IsInt() @Min(0) age: number; @IsEnum(['admin', 'user']) @IsOptional() role?: string; }

启用全局 ValidationPipe

typescript
// main.ts import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes( new ValidationPipe({ whitelist: true, // 自动剥离 DTO 里没定义的字段 forbidNonWhitelisted: true, // 有多余字段时返回 400 而不是静默忽略 transform: true, // 自动把普通对象转成 DTO 类实例(class-transformer 生效) transformOptions: { enableImplicitConversion: true, // 自动类型转换(字符串 → 数字等) }, }), ); await app.listen(3000); }

三个选项都很重要:

  • whitelist: true — 防止客户端传入多余字段(如 isAdmin: true),只保留 DTO 中定义的属性
  • forbidNonWhitelisted: true — 配合 whitelist,有多余字段直接报错,而不是静默丢弃
  • transform: true — 让 class-transformer 的 @Type()@Exclude() 等装饰器生效,否则 DTO 上的装饰器不会被执行

嵌套对象验证

如果 DTO 里有对象类型的属性,必须加 @ValidateNested()@Type()

typescript
import { ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; class AddressDto { @IsString() city: string; @IsString() street: string; } export class CreateUserDto { @IsString() name: string; @ValidateNested() @Type(() => AddressDto) // 必须指定类型,否则 class-transformer 不知道怎么实例化 address: AddressDto; }

不加 @Type() 的话,address 仍然是一个普通 JS 对象,@ValidateNested() 无法对其中的属性做验证。

自定义错误消息

typescript
export class CreateUserDto { @IsString({ message: '用户名必须是字符串' }) name: string; @IsEmail({}, { message: '邮箱格式不正确' }) email: string; }

或者统一格式:

typescript
new ValidationPipe({ exceptionFactory: (errors) => { const messages = errors.map(err => ({ field: err.property, constraints: Object.values(err.constraints || {}), })); return new BadRequestException({ statusCode: 400, message: '输入验证失败', errors: messages }); }, })

自定义管道

内置管道不够用时,写自己的:

typescript
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'; @Injectable() export class ParseDatePipe implements PipeTransform<string, Date> { transform(value: string, metadata: ArgumentMetadata): Date { const date = new Date(value); if (isNaN(date.getTime())) { throw new BadRequestException(`"${value}" 不是有效的日期`); } return date; } }

使用:

typescript
@Get(':date') findByDate(@Param('date', ParseDatePipe) date: Date) { return this.recordsService.findByDate(date); }

管道的作用范围

管道可以用在四个层级:

层级写法影响范围
参数@Param('id', ParseIntPipe)只验证这一个参数
方法@UsePipes(new ValidationPipe())这个路由方法的所有参数
控制器@Controller() @UsePipes(...)这个控制器所有方法
全局app.useGlobalPipes(...)整个应用

全局管道有两种注册方式:

typescript
// 方式一:main.ts 里直接用(无法注入依赖) app.useGlobalPipes(new ValidationPipe()); // 方式二:模块里用 token 注册(可以注入依赖) import { APP_PIPE } from '@nestjs/core'; @Module({ providers: [{ provide: APP_PIPE, useClass: ValidationPipe }], }) export class AppModule {}

如果你的管道需要注入其他服务(如数据库查询),必须用方式二,方式一拿不到依赖注入容器。

请求生命周期中的位置

NestJS 处理一个请求的顺序:

shell
中间件 → 守卫 → 拦截器(before) → 管道 → 控制器 → 拦截器(after) → 过滤器

管道在守卫之后、控制器之前。这意味着守卫可以先判断权限,没权限直接拒绝,不会走到管道的验证逻辑。管道验证失败抛出的异常,会被异常过滤器捕获。

标签:NestJS