创建日期:2026-09-08 | 最近更新:2026-09-08 基于 NestJS 12 +
class-validator0.15 在本机实测:下方所有 HTTP 返回均为对本地 Nest 服务的真实 curl 输出。
DTO / ValidationPipe:把参数校验做成规范
一句话:Nest 把「接口参数校验」提升成了框架规范——你定义一个带
class-validator装饰器的 DTO 类,再挂上内置的ValidationPipe,框架自动完成「白名单过滤 + 类型转换 + 字段校验」,非法请求连 controller 都进不去。这比在每个方法里手写if校验高一个数量级。
1. 三步把它跑起来
① 定义 DTO(带校验规则的一个类)
// cats/dto/create-cat.dto.ts
import { IsInt, IsNotEmpty, IsOptional, IsString, Max, Min } from 'class-validator';
export class CreateCatDto {
@IsString()
@IsNotEmpty()
name!: string;
@IsInt()
@Min(0)
@Max(30)
age!: number;
@IsOptional()
@IsString()
breed?: string;
}
装饰器即规则:@IsInt、@Min/@Max、@IsOptional(可选字段没有它,缺了会报错)。规则库就是 class-validator 那几十个 @Is*。
② 在 controller 用 @Body() dto: XxxDto
@Post()
create(@Body() dto: CreateCatDto): Cat {
return this.catsService.create(dto);
}
③ 全局挂 ValidationPipe(main.ts 一行)
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 剥掉 DTO 里没声明白名单的字段
transform: true, // 把 plain object 转成 DTO 实例(类型收真)
}));
想更规范可做成 provider:
providers: [{ provide: APP_PIPE, useClass: ValidationPipe }]——好处是可注入、可单测。
2. 实测输出(本机 curl,Nest 12)
对上面这套配置发请求,返回都是真实结果:
① POST /cats 合法 body {name:"Tom", age:2, breed:"英短"}
→ 201 {"id":1,"name":"Tom","age":2,"breed":"英短"}
② POST /cats 非法 age:99(DTO 上限 30)
→ 400 {"message":["age must not be greater than 30"],"error":"Bad Request","statusCode":400}
③ POST /cats body 带未声明的 evil:true(whitelist:true)
→ 201 {"id":2,"name":"Kitty","age":1} ← evil 被静默剥掉
④ GET /cats → 200 [上面两条]
三个值得记的输出:
- ②的错误体是标准 Nest 错误结构:
{ message: string[], error, statusCode }——前端拿message数组逐条渲染即可,全后端统一,不用每个接口自己编错误格式; - ③ 证明 whitelist 生效:
evil没在 DTO 里声明,被直接剥掉,不会污染数据层(这正是上一篇「别让脏数据进库」的落地); - 默认
whitelist是静默剥离;想「出现未知字段就报错」用forbidNonWhitelisted: true。
transform:true 的隐藏价值
没有 transform,@Body() 拿到的仍是普通对象(类型是「假装」);开了它,框架会用 class-transformer 的 plainToInstance 把 body 转成 DTO 真实例,于是 dto 上的类型收真、方法可调——配合 @Query/@Param 时,字符串 query 还会被转成数字/布尔等 DTO 声明类型。
3. 进阶:嵌套与「改一半的 DTO」
- 嵌套对象:DTO 里有对象/数组时,光标字段不够,要手动声明递归校验:
import { Type } from 'class-transformer';
import { ValidateNested, IsArray, ArrayMinSize } from 'class-validator';
export class BatchCreateDto {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true }) // 逐元素校验
@Type(() => CreateCatDto) // 告诉转换器每个元素是 CreateCatDto
cats!: CreateCatDto[];
}
- 「更新只用部分字段」:用
@nestjs/mapped-types的PartialType:
import { PartialType } from '@nestjs/mapped-types';
export class UpdateCatDto extends PartialType(CreateCatDto) {}
// 所有字段变可选,其它校验规则继承
4. 什么时候自己写 Pipe
ValidationPipe 覆盖 80% 校验。剩余场景再自定义 Pipe(实现 PipeTransform,处理单值转换/校验):
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string): number {
const n = Number(value);
if (Number.isNaN(n)) throw new BadRequestException(`${value} 不是数字`);
return n;
}
}
用法:@Get(':id') get(@Param('id', ParseIntPipe) id: number)——比手写 Number(id) + try 干净,且可复用。
5. 规范要点(团队约定)
- 每个写/查接口都配 DTO,别用
@Body() body: any——any等于把校验闸门拆了; - 错误体统一 Nest 格式(前端好消费);真要自定义文案,
@IsInt({ message: '…' })逐条给; whitelist: true常开,防「多传字段篡改」;- 列表查询的
@Query也用 DTO +transform,别在 service 里再转类型; - 全局 pipe 配好后,写新接口的「思维负担」几乎为零——DTO 即接口契约。