第 8 章:管道
学习目标
- 理解 Pipe 的两个用途:转换 + 校验
- 使用内置 ValidationPipe
- 写自定义 Pipe
- 避开 4 个校验坑
一、Pipe 是干嘛的
Pipe 在参数传给 Controller 之前执行,可以:
- transform:把 string 转 number、转小写
- validate:校验,失败抛
BadRequestException
执行时机:中间件 → 守卫 → Pipe → Controller。
二、内置 Pipe
| Pipe | 作用 |
|---|---|
ValidationPipe | 用 class-validator 校验 DTO |
ParseIntPipe | 字符串转 int,失败 400 |
ParseFloatPipe | 字符串转 float |
ParseBoolPipe | 字符串转 bool |
ParseUUIDPipe | 校验 UUID |
ParseArrayPipe | 解析数组 |
DefaultValuePipe | 默认值 |
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// id 已经是 number,不是 string
return this.svc.findOne(id);
}访问 GET /users/abc → 自动 400 响应。
三、DefaultValuePipe
@Get()
list(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
) {
return this.svc.findPage(page, limit);
}不带 ?page=2 时,page 默认 1。
⚠️ 坑 1:
ParseIntPipe校验失败会抛BadRequestException,响应格式不是自定义格式,需要全局过滤器改。
四、自定义 Pipe
import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
export class TrimPipe implements PipeTransform<string, string> {
transform(value: string, _meta: ArgumentMetadata): string {
if (typeof value !== 'string') return value;
return value.trim();
}
}
// 使用
@Body(TrimPipe) dto: CreateUserDto4.1 复杂例子:转小写
@Injectable()
export class LowercasePipe implements PipeTransform {
transform(value: any) {
if (typeof value === 'string') return value.toLowerCase();
return value;
}
}五、ValidationPipe + class-validator(强烈推荐)
装包:
pnpm add class-validator class-transformer定义 DTO:
// users/dto/create-user.dto.ts
import { IsString, IsEmail, MinLength, IsOptional, IsInt, Min } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
email: string;
@IsOptional()
@IsInt()
@Min(0)
age?: number;
}Controller:
@Post()
create(@Body() dto: CreateUserDto) {
// dto 已经校验过,直接用
return this.svc.create(dto);
}启用:
// main.ts
import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 剔除未声明字段
forbidNonWhitelisted: true, // 多传字段直接 400
transform: true, // 自动类型转换
}));测试:
curl -X POST http://localhost:3000/users \
-H 'Content-Type: application/json' \
-d '{"name":"T","email":"bad"}'
# {"statusCode":400,"message":["name must be longer than 2 characters","email must be an email"],"error":"Bad Request"}⚠️ 坑 2:忘装
class-validator或忘开transform→ 校验不生效。 ⚠️ 坑 3:没用whitelist: true→ 多余字段会进入数据库,产生脏数据。
六、嵌套对象校验
export class CreateOrderDto {
@IsString()
customer: string;
@ValidateNested()
@Type(() => CreateAddressDto)
address: CreateAddressDto;
}
export class CreateAddressDto {
@IsString() city: string;
@IsString() street: string;
}必须用 @Type(() => XxxDto) 告诉 class-transformer 反序列化嵌套类型。
两个装饰器分工:
| 装饰器 | 来自 | 作用 |
|---|---|---|
@Type(() => XxxDto) | class-transformer | 反序列化 —— 把 JSON 转成 DTO 类的实例 |
@ValidateNested() | class-validator | 递归校验 —— 触发嵌套字段的验证规则 |
执行流程:
HTTP 请求体(纯 JSON)
↓ class-transformer:看到 @Type,转成实例(反序列化)
↓ class-validator:看到 @ValidateNested,递归校验内部字段
↓
返回结果(通过 or 报错)没有 @Type() 会怎样? address 仍是普通对象,class-validator 拿不到类型信息,不会触发校验,脏数据漏过。
"反序列化"是什么? —— 给数据"上户口":
裸数据(无身份) → 上户口(反序列化) → 类的实例(有身份) → 警察(class-validator)才能查
JSON 来的对象是个"路人甲",没有类的身份;反序列化就是把它"上户口"变成 Address 实例,class-validator 才能用 @IsString 这些规则去查。
七、参数级 vs 全局 vs 方法级
// 参数级
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {}
// 方法级
@Post()
@UsePipes(ValidationPipe)
create(@Body() dto: CreateUserDto) {}
// 全局
app.useGlobalPipes(new ValidationPipe());八、实战:Query 转 DTO
@Get()
list(@Query() q: ListUsersQueryDto) { /* q.page/q.limit 已是 number */ }export class ListUsersQueryDto {
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number) // ⚠️ Query 字符串转数字
page: number = 1;
@IsOptional()
@IsInt()
@Type(() => Number)
limit: number = 20;
}⚠️ 坑 4:
@Query()默认是 string,没@Type(() => Number)就校验不过。
@Type + @IsInt 执行顺序详解:
执行顺序和装饰器代码顺序无关,框架自动分两步走:
HTTP 请求 /users?page=2
↓
NestJS 拿到 query: { page: "2" } ← 字符串!第一步:class-transformer(前提:ValidationPipe 开了 transform: true)
- 看到 @Type(() => Number) → 把 "2" 转成 2
↓
第二步:class-validator
- 看到 @IsInt() → 现在是 number → 通过
↓
最终 page = 2(数字)关键点:
transform: true必须开 —— 否则 class-transformer 不跑,@Type()被忽略,@IsInt()会直接报错@Type()先执行 —— 转类型@IsXxx()后执行 —— 校验值
// main.ts
app.useGlobalPipes(new ValidationPipe({ transform: true }));
// ^^^^^^^^^^^^^^^^ 必加没有 transform: true 的后果:
请求 /users?page=2
↓ @Type() 被忽略
↓ page = "2"(字符串)
↓ @IsInt() 校验 → 失败 ❌装饰器代码顺序真相:
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number) // ← 位置随意,不影响执行顺序
page: number = 1;装饰器在代码里怎么排列都行,框架按职责分批执行(先 transform 后 validate),不是按代码顺序。
另一种轻量方案 —— ParseIntPipe 单字段转:
@Get()
list(@Query('page', ParseIntPipe) page: number) {}
// ^^^^^^^^^^^^^
// NestJS 内置,只转单个字段,不依赖 class-transformer记忆口诀:
@Type()→ 先跑,负责转类型@IsXxx()→ 后跑,负责校验值transform: true→ 全局必开,否则@Type()无效
九、本章小结
| 要点 | 关键 |
|---|---|
| 用途 | transform + validate |
| 内置 | ParseIntPipe、ValidationPipe 等 |
| 自定义 | 实现 PipeTransform.transform |
| 校验 | DTO + class-validator + ValidationPipe |
| 全局 | useGlobalPipes 启用 |
| 嵌套 | @ValidateNested + @Type(() => ...) |
动手练习
- DTO 校验:写
CreatePostDto(title minLength 5,content 必填) - 自定义 Pipe:写
ToIntPipe,校验失败抛 400 - 嵌套:写带
tags: string[]的 DTO,验证数组
下一章:第 9 章:守卫 →