在之前,我们学习了拦截器(Interceptor),了解了如何在响应返回前进行数据转换。这节课,我们将深入学习 管道(Pipe),这是 NestJS 中用于数据验证和转换的核心机制。
管道在数据到达控制器之前执行,可以对请求参数进行验证、转换和清理。如果你熟悉前端的表单验证(如 Element UI 的表单校验),那么理解 NestJS 管道会非常轻松 🚀
一、什么是管道?
管道的定义 🎯
管道(Pipe) 是在数据到达控制器之前进行验证和转换的类。它实现了 PipeTransform 接口,是 NestJS 请求处理流程中的重要一环。
管道主要用于三大场景:
- 数据转换:将字符串转换为数字、日期等类型
- 数据验证:验证数据是否符合业务规则
- 数据清理:清理和规范化输入数据
📚 参考资源:
- NestJS Pipes 官方文档 - 官方完整指南
- class-validator GitHub - 验证装饰器库
管道的执行时机 📍
在 NestJS 的请求处理流程中,管道处于关键位置:
请求 → 中间件 → 守卫 → 拦截器(前) → 管道 → 控制器管道在拦截器之后、控制器之前执行,确保控制器接收到的数据已经过验证和转换。
管道的分类
NestJS 提供了两种类型的管道:
- 内置管道:框架提供的开箱即用的管道,如
ParseIntPipe、ValidationPipe等 - 自定义管道:根据业务需求自行创建的管道
二、内置管道详解
NestJS 提供了一系列内置管道,涵盖了常见的数据转换和验证场景。
ParseIntPipe - 整数转换
将字符串参数转换为整数类型,常用于路由参数和查询参数的处理:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// id 自动转换为 number 类型
return this.userService.findOne(id);
}如果参数不是有效的数字,管道会自动抛出 BadRequestException,返回 400 错误。
ParseFloatPipe - 浮点数转换
处理需要精确小数的场景,如价格、评分等:
@Get(':price')
getPrice(@Param('price', ParseFloatPipe) price: number) {
return { price };
}ParseBoolPipe - 布尔值转换
将字符串 'true' 或 'false' 转换为布尔类型:
@Get()
findAll(@Query('active', ParseBoolPipe) active: boolean) {
return this.userService.findAll({ active });
}ParseUUIDPipe - UUID 验证
验证参数是否符合 UUID 格式,常用于数据库主键验证:
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
// id 必须是有效的 UUID 格式(如:550e8400-e29b-41d4-a716-446655440000)
return this.userService.findOne(id);
}ParseEnumPipe - 枚举值验证
确保参数值在预定义的枚举范围内:
enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest',
}
@Get(':role')
findByRole(
@Param('role', new ParseEnumPipe(UserRole)) role: UserRole,
) {
// role 必须是枚举中定义的值
return this.userService.findByRole(role);
}DefaultValuePipe - 默认值设置
为可选参数提供默认值,常用于分页场景:
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
// 如果 page 或 limit 不存在,使用默认值
return this.userService.findAll(page, limit);
}三、class-validator 与 class-transformer
依赖安装
首先,安装必要的依赖包:
pnpm add class-validator@0.14.2 class-transformer@0.5.1📚 参考资源:
- class-validator GitHub - 基于装饰器的验证库
- class-transformer GitHub - 对象转换工具
核心概念理解
这两个库是 NestJS 数据验证的基石:
- class-validator:通过装饰器定义验证规则,提供丰富的内置验证器(如
@IsEmail()、@MinLength()等) - class-transformer:将普通 JavaScript 对象转换为类实例,使验证装饰器生效
基本使用示例
import { IsString, IsEmail, IsNotEmpty } from 'class-validator';
export class CreateUserDto {
@IsString() // 验证是否为字符串
@IsNotEmpty() // 验证是否不为空
name: string;
@IsEmail() // 验证邮箱格式
@IsNotEmpty()
email: string;
}四、DTO(数据传输对象)
什么是 DTO?
DTO(Data Transfer Object) 是数据传输对象,用于定义 API 接口的数据结构和验证规则。它是前后端数据交互的契约。
📚 参考资源:
- DTO 模式详解 - 设计模式介绍
为什么需要 DTO?💡
使用 DTO 带来的核心优势:
- ✅ 类型安全:TypeScript 编译时类型检查,减少运行时错误
- ✅ 数据验证:自动验证请求数据,防止非法数据进入系统
- ✅ 文档生成:配合 Swagger 等工具自动生成 API 文档
- ✅ 代码清晰:明确接口的输入输出结构,提高可维护性
创建完整的 DTO
下面是一个用户注册 DTO 的完整示例,展示了常见的验证场景:
// src/user/dto/create-user.dto.ts
import {
IsString,
IsEmail,
IsNotEmpty,
MinLength,
MaxLength,
Matches,
} from 'class-validator';
export class CreateUserDto {
@IsString({ message: '用户名必须是字符串' })
@IsNotEmpty({ message: '用户名不能为空' })
@MinLength(2, { message: '用户名至少2个字符' })
@MaxLength(20, { message: '用户名最多20个字符' })
@Matches(/^[a-zA-Z0-9_\u4e00-\u9fa5]+$/, {
message: '用户名只能包含字母、数字、下划线和中文',
})
name: string;
@IsEmail({}, { message: '邮箱格式不正确' })
@IsNotEmpty({ message: '邮箱不能为空' })
email: string;
@IsString({ message: '密码必须是字符串' })
@IsNotEmpty({ message: '密码不能为空' })
@MinLength(6, { message: '密码至少6个字符' })
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: '密码必须包含大小写字母和数字',
})
password: string;
}在控制器中使用
// src/user/user.controller.ts
import { CreateUserDto } from './dto/create-user.dto';
@Post('register')
create(@Body() createUserDto: CreateUserDto) {
// createUserDto 已经过验证,可以安全使用
return this.userService.create(createUserDto);
}五、ValidationPipe 配置
全局配置
在 main.ts 中配置全局 ValidationPipe,对所有接口生效:
// src/main.ts
import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 自动移除 DTO 中未声明的字段
forbidNonWhitelisted: true, // 有未声明字段就报错
transform: true, // 自动类型转换
transformOptions: {
enableImplicitConversion: true, // 启用隐式转换
},
}),
);配置选项详解
| 选项 | 类型 | 说明 | 使用场景 |
|---|---|---|---|
whitelist | boolean | 自动移除未声明字段 | 防止多余数据进入系统,提高安全性 |
forbidNonWhitelisted | boolean | 有未声明字段就报错 | 严格模式,明确拒绝意外数据 |
transform | boolean | 自动类型转换 | 将普通对象转换为 DTO 类实例 |
enableImplicitConversion | boolean | 启用隐式转换 | 自动将 "123" 转换为 123 |
局部配置
也可以在控制器或方法级别单独配置:
@Post()
@UsePipes(
new ValidationPipe({
whitelist: true,
transform: true,
}),
)
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}六、常用验证装饰器
字符串验证
// 基础验证
@IsString() // 验证是否是字符串
@IsNotEmpty() // 验证是否不为空
// 长度验证
@Length(2, 20) // 验证长度在 2-20 之间
@MinLength(2) // 最小长度 2
@MaxLength(20) // 最大长度 20
// 正则验证
@Matches(/^[a-zA-Z0-9]+$/) // 匹配正则表达式数字验证
@IsNumber() // 验证是否是数字
@IsInt() // 验证是否是整数
@Min(0) // 最小值 0
@Max(120) // 最大值 120
@IsPositive() // 验证是否是正数
@IsNegative() // 验证是否是负数邮箱和 URL 验证
@IsEmail() // 验证邮箱格式
@IsUrl() // 验证 URL 格式📚 参考资源:
- Email 验证标准 RFC 5322 - 邮箱格式规范
日期验证
@IsDate() // 验证是否是日期对象
@IsDateString() // 验证是否是日期字符串(ISO 8601)数组验证
@IsArray() // 验证是否是数组
@ArrayMinSize(1) // 数组最小长度 1
@ArrayMaxSize(10) // 数组最大长度 10
@ArrayNotEmpty() // 数组不能为空可选字段
@IsOptional() // 标记字段为可选
@IsString()
nickname?: string;枚举验证
enum UserRole {
ADMIN = 'admin',
USER = 'user',
}
@IsEnum(UserRole) // 验证枚举值
role: UserRole;七、实战:用户注册数据验证
需求分析 📋
创建一个用户注册接口,包含以下验证规则:
- 用户名:必填,2-20个字符,只能包含字母、数字、下划线和中文
- 邮箱:必填,有效的邮箱格式
- 密码:必填,至少6个字符,必须包含大小写字母和数字
完整实现
第一步:创建 DTO
// src/user/dto/create-user.dto.ts
import {
IsString,
IsEmail,
IsNotEmpty,
MinLength,
MaxLength,
Matches,
} from 'class-validator';
export class CreateUserDto {
@IsString({ message: '用户名必须是字符串' })
@IsNotEmpty({ message: '用户名不能为空' })
@MinLength(2, { message: '用户名至少2个字符' })
@MaxLength(20, { message: '用户名最多20个字符' })
@Matches(/^[a-zA-Z0-9_\u4e00-\u9fa5]+$/, {
message: '用户名只能包含字母、数字、下划线和中文',
})
name: string;
@IsEmail({}, { message: '邮箱格式不正确' })
@IsNotEmpty({ message: '邮箱不能为空' })
email: string;
@IsString({ message: '密码必须是字符串' })
@IsNotEmpty({ message: '密码不能为空' })
@MinLength(6, { message: '密码至少6个字符' })
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: '密码必须包含大小写字母和数字',
})
password: string;
}第二步:在控制器中使用
// src/user/user.controller.ts
import { CreateUserDto } from './dto/create-user.dto';
@Post('register')
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}第三步:配置全局 ValidationPipe
// src/main.ts
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);测试验证 ✅
测试正确的数据:
curl -X POST http://localhost:3000/user/register \
-H "Content-Type: application/json" \
-d '{
"name": "testuser",
"email": "test@example.com",
"password": "Test123"
}'测试错误的数据:
curl -X POST http://localhost:3000/user/register \
-H "Content-Type: application/json" \
-d '{
"name": "a",
"email": "invalid-email",
"password": "123"
}'错误响应示例:
{
"statusCode": 400,
"message": [
"用户名至少2个字符",
"邮箱格式不正确",
"密码至少6个字符",
"密码必须包含大小写字母和数字"
],
"error": "Bad Request"
}八、嵌套对象与数组验证
验证嵌套对象
使用 @ValidateNested() 和 @Type() 验证嵌套对象:
import { ValidateNested, IsObject } from 'class-validator';
import { Type } from 'class-transformer';
class AddressDto {
@IsString()
@IsNotEmpty()
street: string;
@IsString()
@IsNotEmpty()
city: string;
@IsString()
@IsNotEmpty()
zipCode: string;
}
export class CreateUserDto {
@IsString()
name: string;
@ValidateNested() // 验证嵌套对象
@Type(() => AddressDto) // 指定类型
@IsObject()
address: AddressDto;
}验证对象数组
import { IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
class TagDto {
@IsString()
@IsNotEmpty()
name: string;
}
export class CreatePostDto {
@IsString()
@IsNotEmpty()
title: string;
@IsArray()
@ValidateNested({ each: true }) // 验证数组中的每个元素
@Type(() => TagDto)
tags: TagDto[];
}九、自定义管道
创建自定义管道
当内置管道无法满足需求时,可以创建自定义管道。下面是一个验证手机号的示例:
// src/common/pipes/phone-validation.pipe.ts
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';
@Injectable()
export class PhoneValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
if (!value) {
throw new BadRequestException('手机号不能为空');
}
// 中国大陆手机号验证
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(value)) {
throw new BadRequestException('手机号格式不正确');
}
return value;
}
}使用自定义管道
@Post()
create(@Body('phone', PhoneValidationPipe) phone: string) {
// phone 已经过验证,是有效的手机号
return this.userService.create({ phone });
}管道参数详解
transform 方法接收两个参数:
transform(value: any, metadata: ArgumentMetadata) {
// value: 要转换的值
// metadata: 元数据对象
console.log('值:', value);
console.log('类型:', metadata.type); // 'body' | 'query' | 'param' | 'custom'
console.log('元数据:', metadata.metatype); // 参数的类型
console.log('参数名:', metadata.data); // 参数名(如果指定了)
return value;
}十、数据转换
@Transform() 装饰器
使用 @Transform() 装饰器在验证前转换数据:
import { Transform } from 'class-transformer';
export class CreateUserDto {
@Transform(({ value }) => value.trim()) // 去除首尾空格
@IsString()
username: string;
@Transform(({ value }) => value.toLowerCase()) // 转换为小写
@IsEmail()
email: string;
}@Exclude() 和 @Expose()
控制序列化时哪些字段被包含或排除:
import { Exclude, Expose } from 'class-transformer';
export class UserDto {
@Expose()
id: number;
@Expose()
name: string;
@Exclude() // 排除密码字段,防止泄露
password: string;
}@Type() 装饰器
指定嵌套对象的类型,确保正确转换:
import { Type } from 'class-transformer';
export class CreateUserDto {
@Type(() => Date) // 将字符串转换为 Date 对象
birthDate: Date;
@Type(() => AddressDto) // 指定嵌套对象类型
address: AddressDto;
}十一、最佳实践
1. 使用全局 ValidationPipe 🌐
对所有接口统一配置验证规则:
// ✅ 推荐:全局配置
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);2. 提供友好的错误消息 💬
// ✅ 推荐:友好的错误消息
@IsString({ message: '用户名必须是字符串' })
@MinLength(2, { message: '用户名至少需要2个字符,请重新输入' })
username: string;
// ❌ 避免:使用默认消息
@IsString()
@MinLength(2)
username: string;3. 合理使用 DTO 继承 🔄
// ✅ 推荐:复用 DTO
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
}
// 更新 DTO 继承创建 DTO,所有字段自动变为可选
export class UpdateUserDto extends PartialType(CreateUserDto) {}4. 使用常量定义验证规则 📏
// ✅ 推荐:使用常量
const USERNAME_MIN_LENGTH = 2;
const USERNAME_MAX_LENGTH = 20;
const USERNAME_PATTERN = /^[a-zA-Z0-9_\u4e00-\u9fa5]+$/;
export class CreateUserDto {
@MinLength(USERNAME_MIN_LENGTH)
@MaxLength(USERNAME_MAX_LENGTH)
@Matches(USERNAME_PATTERN)
username: string;
}5. 创建自定义验证装饰器 🎯
对于复杂的验证逻辑,创建可复用的自定义装饰器:
// ✅ 推荐:自定义装饰器
import { registerDecorator, ValidationOptions } from 'class-validator';
export function IsStrongPassword(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isStrongPassword',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any) {
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/.test(value);
},
defaultMessage() {
return '密码必须包含大小写字母、数字和特殊字符';
},
},
});
};
}
// 使用
export class CreateUserDto {
@IsStrongPassword()
password: string;
}总结 🎯
这节课我们系统学习了 NestJS 管道的核心概念和实战应用。让我来总结一下要点:
核心知识点 ✅
- ✅ 理解了管道的定义:在数据到达控制器前进行验证和转换的类
- ✅ 掌握了内置管道:
ParseIntPipe、ParseFloatPipe、ValidationPipe等常用管道 - ✅ 学习了 class-validator:掌握了各种验证装饰器的使用方法
- ✅ 理解了 DTO 概念:数据传输对象的定义和使用场景
- ✅ 掌握了 ValidationPipe 配置:
whitelist、transform等核心选项 - ✅ 学习了嵌套对象验证:使用
@ValidateNested()和@Type()处理复杂结构 - ✅ 掌握了自定义管道:创建符合业务需求的验证逻辑
- ✅ 学习了数据转换:使用 class-transformer 进行数据转换
常用验证装饰器速查表 📊
| 类型 | 装饰器 | 说明 |
|---|---|---|
| 字符串 | @IsString() | 验证是否是字符串 |
@IsNotEmpty() | 验证是否不为空 | |
@MinLength(n) | 最小长度 | |
@MaxLength(n) | 最大长度 | |
@Matches(regex) | 正则匹配 | |
| 数字 | @IsNumber() | 验证是否是数字 |
@IsInt() | 验证是否是整数 | |
@Min(n) | 最小值 | |
@Max(n) | 最大值 | |
| 其他 | @IsEmail() | 验证邮箱格式 |
@IsUrl() | 验证 URL 格式 | |
@IsEnum(enum) | 验证枚举值 | |
@IsArray() | 验证是否是数组 | |
@IsOptional() | 标记为可选字段 |
延伸学习资源 📚
- NestJS 官方文档 - Pipes - 官方完整指南
- class-validator 官方文档 - 验证装饰器完整列表
- class-transformer 官方文档 - 对象转换工具
- DTO 模式详解 - 设计模式介绍
- 本文链接:https://fridolph.top/posts/2025-11-04__nest11-pipe
- 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 许可协议。