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 做请求体验证
这是实际项目里用得最多的验证方式。
安装依赖
bashnpm install class-validator class-transformer
定义 DTO
typescriptimport { 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():
typescriptimport { 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() 无法对其中的属性做验证。
自定义错误消息
typescriptexport class CreateUserDto { @IsString({ message: '用户名必须是字符串' }) name: string; @IsEmail({}, { message: '邮箱格式不正确' }) email: string; }
或者统一格式:
typescriptnew ValidationPipe({ exceptionFactory: (errors) => { const messages = errors.map(err => ({ field: err.property, constraints: Object.values(err.constraints || {}), })); return new BadRequestException({ statusCode: 400, message: '输入验证失败', errors: messages }); }, })
自定义管道
内置管道不够用时,写自己的:
typescriptimport { 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) → 过滤器
管道在守卫之后、控制器之前。这意味着守卫可以先判断权限,没权限直接拒绝,不会走到管道的验证逻辑。管道验证失败抛出的异常,会被异常过滤器捕获。