【NestJS】11 管道:数据验证与转换实战

3413 字
17 分钟
【NestJS】11 管道:数据验证与转换实战

在之前,我们学习了拦截器(Interceptor),了解了如何在响应返回前进行数据转换。这节课,我们将深入学习 管道(Pipe),这是 NestJS 中用于数据验证和转换的核心机制。

管道在数据到达控制器之前执行,可以对请求参数进行验证、转换和清理。如果你熟悉前端的表单验证(如 Element UI 的表单校验),那么理解 NestJS 管道会非常轻松 🚀

一、什么是管道?#

管道的定义 🎯#

管道(Pipe) 是在数据到达控制器之前进行验证和转换的类。它实现了 PipeTransform 接口,是 NestJS 请求处理流程中的重要一环。

管道主要用于三大场景:

  • 数据转换:将字符串转换为数字、日期等类型
  • 数据验证:验证数据是否符合业务规则
  • 数据清理:清理和规范化输入数据

📚 参考资源

管道的执行时机 📍#

在 NestJS 的请求处理流程中,管道处于关键位置:

请求 → 中间件 → 守卫 → 拦截器(前) → 管道 → 控制器

管道在拦截器之后、控制器之前执行,确保控制器接收到的数据已经过验证和转换。

管道的分类#

NestJS 提供了两种类型的管道:

  • 内置管道:框架提供的开箱即用的管道,如 ParseIntPipeValidationPipe
  • 自定义管道:根据业务需求自行创建的管道

二、内置管道详解#

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#

依赖安装#

首先,安装必要的依赖包:

Terminal window
pnpm add class-validator@0.14.2 class-transformer@0.5.1

📚 参考资源

核心概念理解#

这两个库是 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 带来的核心优势:

  • 类型安全: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, // 启用隐式转换
},
}),
);

配置选项详解#

选项类型说明使用场景
whitelistboolean自动移除未声明字段防止多余数据进入系统,提高安全性
forbidNonWhitelistedboolean有未声明字段就报错严格模式,明确拒绝意外数据
transformboolean自动类型转换将普通对象转换为 DTO 类实例
enableImplicitConversionboolean启用隐式转换自动将 "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 格式

📚 参考资源

日期验证#

@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,
},
}),
);

测试验证 ✅#

测试正确的数据:

Terminal window
curl -X POST http://localhost:3000/user/register \
-H "Content-Type: application/json" \
-d '{
"name": "testuser",
"email": "test@example.com",
"password": "Test123"
}'

测试错误的数据:

Terminal window
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 管道的核心概念和实战应用。让我来总结一下要点:

核心知识点 ✅#

  • 理解了管道的定义:在数据到达控制器前进行验证和转换的类
  • 掌握了内置管道ParseIntPipeParseFloatPipeValidationPipe 等常用管道
  • 学习了 class-validator:掌握了各种验证装饰器的使用方法
  • 理解了 DTO 概念:数据传输对象的定义和使用场景
  • 掌握了 ValidationPipe 配置whitelisttransform 等核心选项
  • 学习了嵌套对象验证:使用 @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】11 管道:数据验证与转换实战
https://blog.fridolph.top/posts/2025-11-04__nest11-pipe/
作者
Fridolph
发布于
2025-11-04
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录