Skip to content
第 173 / 250 章Node⏱ 10 分钟阅读

第 173 章:管道(Pipe)与数据转换

学习目标

  • 理解 Pipe 的作用
  • 掌握内置 Pipe
  • 学会自定义 Pipe
  • 掌握 class-validator 数据验证

一、什么是 Pipe

Pipe 用来转换验证控制器接收的输入数据。

两个核心用途:

  • 转换:把输入数据转成期望类型
  • 验证:检查数据是否合法,失败抛异常

二、内置 Pipe

2.1 转换类

typescript
import { ParseIntPipe, ParseFloatPipe, ParseBoolPipe, ParseArrayPipe, ParseUUIDPipe, ParseEnumPipe, DefaultValuePipe } from '@nestjs/common';

class DemoController {
  // 字符串 → 数字,失败 400
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {}

  // 字符串 → 浮点数
  @Get('price/:price')
  price(@Param('price', ParseFloatPipe) price: number) {}

  // 字符串 → boolean
  @Get('active/:flag')
  active(@Param('flag', ParseBoolPipe) flag: boolean) {}

  // 字符串数组 → 数组
  @Get('list')
  list(@Query('ids', ParseArrayPipe) ids: string[]) {}

  // 字符串 → UUID
  @Get(':uuid')
  one(@Param('uuid', ParseUUIDPipe) uuid: string) {}

  // 字符串 → 枚举值
  @Get('role/:role')
  role(@Param('role', new ParseEnumPipe(Role)) role: Role) {}

  // 默认值
  @Get('list')
  list(@Query('page', new DefaultValuePipe(1)) page: number) {}
}

2.2 验证类

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

// main.ts 全局启用
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,       // 剥离未声明字段
  forbidNonWhitelisted: true,  // 遇到未知字段报错
  transform: true,       // 自动类型转换
}));

三、class-validator 装饰器

3.1 安装

bash
pnpm add class-validator class-transformer

3.2 基础验证

typescript
import { IsString, IsInt, IsEmail, MinLength, MaxLength, Min, Max, IsOptional, IsEnum } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  @MaxLength(20)
  name: string;

  @IsEmail()
  email: string;

  @IsInt()
  @Min(0)
  @Max(150)
  age: number;

  @IsOptional()           // 可选
  @IsString()
  phone?: string;

  @IsEnum(['admin', 'user'])
  role: 'admin' | 'user';
}

3.3 嵌套验证

typescript
export class CreateOrderDto {
  @IsString()
  productId: string;

  @ValidateNested()
  @Type(() => AddressDto)   // class-transformer
  address: AddressDto;
}

export class AddressDto {
  @IsString()
  city: string;

  @IsString()
  street: string;
}

3.4 数组验证

typescript
import { ArrayMinSize, ArrayMaxSize, IsArray } from 'class-validator';

export class BatchCreateDto {
  @IsArray()
  @ArrayMinSize(1)
  @ArrayMaxSize(100)
  @ValidateNested({ each: true })
  @Type(() => CreateUserDto)
  users: CreateUserDto[];
}

四、自定义 Pipe

4.1 实现 PipeTransform

typescript
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class TrimPipe implements PipeTransform {
  // value:被转换的值
  // metadata:元数据
  transform(value: any, metadata: ArgumentMetadata) {
    if (typeof value !== 'string') return value;
    return value.trim();
  }
}

// 使用
@Post()
create(@Body(TrimPipe) dto: CreateUserDto) {}

4.2 参数转换 Pipe

typescript
@Injectable()
export class ToNumberPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException(`${value} is not a number`);
    }
    return val;
  }
}

@Get(':id')
findOne(@Param('id', ToNumberPipe) id: number) {}

4.3 全局验证配置

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

app.useGlobalPipes(new ValidationPipe({
  whitelist: true,
  forbidNonWhitelisted: true,
  transform: true,
  transformOptions: {
    enableImplicitConversion: true,  // 隐式类型转换
  },
  disableErrorMessages: false,       // 是否隐藏错误
  stopAtFirstError: false,           // 是否第一个错误就停止
}));

五、参数级别 vs 方法级别 vs 全局级别

5.1 参数级(单个)

typescript
@Post()
create(@Body(ValidationPipe) dto: CreateUserDto) {}

5.2 方法级(整个方法)

typescript
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
create(@Body() dto: CreateUserDto) {}

5.3 控制器级

typescript
@Controller('users')
@UsePipes(ValidationPipe)
export class UserController {
  @Post()
  create(@Body() dto: CreateUserDto) {}
}

5.4 全局级(推荐)

typescript
// main.ts
app.useGlobalPipes(new ValidationPipe());

六、自定义验证装饰器

6.1 简单装饰器

typescript
import { registerDecorator, ValidationOptions } from 'class-validator';

export function IsStrongPassword(options?: ValidationOptions) {
  return function (object: object, propertyName: string) {
    registerDecorator({
      name: 'isStrongPassword',
      target: object.constructor,
      propertyName,
      options,
      validator: {
        validate(value: any) {
          return typeof value === 'string'
            && /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(value);
        },
        defaultMessage() {
          return '密码必须 8 位以上,包含大小写字母和数字';
        },
      },
    });
  };
}

// 使用
export class RegisterDto {
  @IsStrongPassword()
  password: string;
}

6.2 复杂约束

typescript
// 两个字段必须相同
export function IsMatch(property: string, options?: ValidationOptions) {
  return function (object: object, propertyName: string) {
    registerDecorator({
      name: 'isMatch',
      target: object.constructor,
      propertyName,
      constraints: [property],
      options,
      validator: {
        validate(value, args) {
          const [relatedPropertyName] = args.constraints;
          const relatedValue = (args.object as any)[relatedPropertyName];
          return value === relatedValue;
        },
      },
    });
  };
}

export class RegisterDto {
  @IsString()
  @MinLength(6)
  password: string;

  @IsMatch('password')
  confirmPassword: string;
}

七、文件验证 Pipe

typescript
@Injectable()
export class FileSizeValidationPipe implements PipeTransform {
  transform(file: Express.Multer.File) {
    const maxSize = 5 * 1024 * 1024; // 5MB
    if (file.size > maxSize) {
      throw new BadRequestException('文件大小不能超过 5MB');
    }
    return file;
  }
}

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
upload(@UploadedFile(FileSizeValidationPipe) file: Express.Multer.File) {}

八、异常处理

typescript
// 自定义异常消息
export class CreateUserDto {
  @IsString({ message: '姓名必须是字符串' })
  name: string;

  @IsInt({ message: '年龄必须是整数' })
  @Min(0, { message: '年龄不能小于 0' })
  age: number;
}

// 响应示例
{
  "statusCode": 400,
  "message": [
    "姓名必须是字符串",
    "年龄不能小于 0"
  ],
  "error": "Bad Request"
}

九、分页 DTO

typescript
export class PaginationDto {
  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Min(1)
  page: number = 1;

  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Min(1)
  @Max(100)
  size: number = 10;

  @IsOptional()
  @IsString()
  sort?: string;
}

@Get()
list(@Query() pagination: PaginationDto) {
  return this.userService.findAll(pagination);
}

重要

@Type(() => Number) 来自 class-transformer,必须装。

十、本章小结

类型例子
转换 PipeParseIntPipe ParseUUIDPipe
验证 PipeValidationPipe
默认值DefaultValuePipe
验证装饰器@IsString @IsEmail
自定义验证registerDecorator
作用范围参数/方法/控制器/全局

动手练习

  1. 创建一个 RegisterDto,验证邮箱、密码、确认密码
  2. 写一个 TrimPipe 自动去掉字符串首尾空格
  3. 实现一个分页 DTO,带默认值和范围校验
  4. 写一个 @IsCNPhone 自定义验证装饰器

推荐阅读


下一章:第 174 章:守卫(Guard)与权限控制

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