Skip to content
第 8 章 后端 ⏱ 13 分钟阅读

第 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默认值
typescript
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  // id 已经是 number,不是 string
  return this.svc.findOne(id);
}

访问 GET /users/abc → 自动 400 响应。

三、DefaultValuePipe ​

typescript
@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 ​

typescript
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: CreateUserDto

4.1 复杂例子:转小写 ​

typescript
@Injectable()
export class LowercasePipe implements PipeTransform {
  transform(value: any) {
    if (typeof value === 'string') return value.toLowerCase();
    return value;
  }
}

五、ValidationPipe + class-validator(强烈推荐) ​

装包:

bash
pnpm add class-validator class-transformer

定义 DTO:

typescript
// 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:

typescript
@Post()
create(@Body() dto: CreateUserDto) {
  // dto 已经校验过,直接用
  return this.svc.create(dto);
}

启用:

typescript
// main.ts
import { ValidationPipe } from '@nestjs/common';

app.useGlobalPipes(new ValidationPipe({
  whitelist: true,                 // 剔除未声明字段
  forbidNonWhitelisted: true,      // 多传字段直接 400
  transform: true,                 // 自动类型转换
}));

测试:

bash
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 → 多余字段会进入数据库,产生脏数据。

六、嵌套对象校验 ​

typescript
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 方法级 ​

typescript
// 参数级
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {}

// 方法级
@Post()
@UsePipes(ValidationPipe)
create(@Body() dto: CreateUserDto) {}

// 全局
app.useGlobalPipes(new ValidationPipe());

八、实战:Query 转 DTO ​

typescript
@Get()
list(@Query() q: ListUsersQueryDto) { /* q.page/q.limit 已是 number */ }
typescript
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() 后执行 —— 校验值
typescript
// main.ts
app.useGlobalPipes(new ValidationPipe({ transform: true }));
//                                    ^^^^^^^^^^^^^^^^ 必加

没有 transform: true 的后果:

请求 /users?page=2
   ↓ @Type() 被忽略
   ↓ page = "2"(字符串)
   ↓ @IsInt() 校验 → 失败 ❌

装饰器代码顺序真相:

typescript
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number) // ← 位置随意,不影响执行顺序
page: number = 1;

装饰器在代码里怎么排列都行,框架按职责分批执行(先 transform 后 validate),不是按代码顺序。

另一种轻量方案 —— ParseIntPipe 单字段转:

typescript
@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(() => ...)

动手练习 ​

  1. DTO 校验:写 CreatePostDto(title minLength 5,content 必填)
  2. 自定义 Pipe:写 ToIntPipe,校验失败抛 400
  3. 嵌套:写带 tags: string[] 的 DTO,验证数组

下一章:第 9 章:守卫 →

本站基于 VitePress 构建 · 由 StackHub 团队维护