NestJS数据校验管道ValidationPipe详解
NestJS数据校验管道ValidationPipe详解
学习目标:理解数据校验的重要性、掌握 ValidationPipe 工作原理、熟练使用 class-validator 和 class-transformer 进行数据校验。
一、数据校验的重要性
1.1 为什么需要数据校验?
数据校验的重要性:
│
├── 1. 数据安全性
│ ├── 防止恶意数据注入
│ ├── 防止 SQL 注入攻击
│ └── 防止 XSS 攻击
│
├── 2. 数据完整性
│ ├── 确保必填字段不为空
│ ├── 确保数据格式正确
│ └── 确保数据类型正确
│
├── 3. 业务逻辑正确性
│ ├── 确保数据符合业务规则
│ ├── 防止非法数据进入业务逻辑
│ └── 提前拦截无效请求
│
└── 4. 性能优化
├── 减少无效的数据库查询
├── 减少服务器资源消耗
└── 提前返回错误,避免后续处理1.2 数据校验的位置
数据校验的两个位置:
│
├── 前端校验
│ ├── 目的:提升用户体验
│ ├── 优点:快速反馈,无需请求服务器
│ ├── 缺点:可以被绕过(禁用 JavaScript)
│ └── 结论:前端校验不是安全的保障
│
└── 后端校验
├── 目的:保障数据安全
├── 优点:无法绕过,安全可靠
├── 必须性:必须进行后端校验
└── 结论:后端校验是最后一道防线重要原则:
- 前端校验 + 后端校验 = 最佳实践
- 只做前端校验 = 不安全
- 后端校验是必须的
二、NestJS ValidationPipe 概述
2.1 NestJS 内置的 Pipe
NestJS 提供了多种内置 Pipe:
| Pipe | 类型 | 作用 |
|---|---|---|
ValidationPipe | 校验 | 数据校验(结合 class-validator) |
ParseIntPipe | 转换 | string → number |
ParseFloatPipe | 转换 | string → float |
ParseBoolPipe | 转换 | string → boolean |
ParseArrayPipe | 转换 | string → array |
ParseUUIDPipe | 校验 | 验证 UUID 格式 |
ParseEnumPipe | 校验 | 验证枚举值 |
2.2 ValidationPipe 的作用
ValidationPipe 的作用:
│
├── 1. 数据校验
│ ├── 校验必填字段
│ ├── 校验数据类型
│ ├── 校验数据格式
│ └── 校验业务规则
│
├── 2. 数据转换
│ ├── JSON 对象 → Class 实例
│ ├── 自动去除多余字段
│ └── 自动类型转换
│
├── 3. 错误处理
│ ├── 自动抛出校验错误
│ ├── 返回详细的错误信息
│ └── 统一的错误格式
│
└── 4. 提前拦截
├── 校验失败不进入 Controller
├── 减少后续处理压力
└── 提升系统安全性三、核心依赖库详解
3.1 class-validator 简介
官方定义:基于装饰器的类型校验工具,用于校验 class 类的属性。
GitHub 地址:https://github.com/typestack/class-validator
核心功能:
class-validator 的核心功能:
│
├── 1. 装饰器校验
│ ├── @IsString() - 校验字符串
│ ├── @IsNumber() - 校验数字
│ ├── @IsEmail() - 校验邮箱
│ ├── @IsInt() - 校验整数
│ ├── @IsBoolean() - 校验布尔值
│ ├── @IsDate() - 校验日期
│ ├── @IsArray() - 校验数组
│ ├── @IsEnum() - 校验枚举
│ ├── @Min() - 最小值
│ ├── @Max() - 最大值
│ ├── @MinLength() - 最小长度
│ ├── @MaxLength() - 最大长度
│ ├── @IsNotEmpty() - 非空校验
│ ├── @IsOptional() - 可选字段
│ └── ... 更多装饰器
│
├── 2. 嵌套校验
│ ├── @ValidateNested() - 嵌套对象校验
│ └── @Type() - 类型转换
│
├── 3. 自定义校验
│ ├── @ValidatorConstraint() - 自定义校验器
│ └── @ValidateBy() - 自定义校验规则
│
└── 4. 分组校验
└── groups: ['create', 'update'] - 不同场景校验基本使用示例:
import { IsString, IsInt, IsEmail, Min, Max, IsNotEmpty } from 'class-validator';
export class CreateUserDto {
@IsString({ message: 'name 必须是字符串' })
@IsNotEmpty({ message: 'name 不能为空' })
name: string;
@IsInt({ message: 'age 必须是整数' })
@Min(0, { message: 'age 不能小于 0' })
@Max(150, { message: 'age 不能大于 150' })
age: number;
@IsEmail({}, { message: 'email 格式不正确' })
email: string;
}3.2 class-transformer 简介
官方定义:基于装饰器的对象转换、序列化、反序列化工具。
GitHub 地址:https://github.com/typestack/class-transformer
核心功能:
class-transformer 的核心功能:
│
├── 1. 对象转换
│ ├── plainToClass() - 普通对象 → Class 实例
│ ├── plainToInstance() - 普通对象 → Class 实例(推荐)
│ ├── classToPlain() - Class 实例 → 普通对象
│ └── instanceToPlain() - Class 实例 → 普通对象(推荐)
│
├── 2. 装饰器
│ ├── @Expose() - 暴露属性
│ ├── @Exclude() - 排除属性
│ ├── @Type() - 类型转换
│ ├── @Transform() - 自定义转换
│ └── @Exclude() - 排除属性
│
└── 3. 序列化控制
├── 控制哪些属性被序列化
├── 控制哪些属性被反序列化
└── 支持嵌套对象转换为什么需要 class-transformer?
// 问题:普通的 JSON 对象无法使用实例方法
const userJson = {
id: 1,
firstName: 'Tom',
lastName: 'Smith',
};
// 访问属性需要使用对象属性访问
console.log(userJson['firstName']); // Tom
console.log(userJson.firstName); // Tom
// 解决:使用 class-transformer 转换为实例
class User {
id: number;
firstName: string;
lastName: string;
// 可以定义实例方法
getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
const user = plainToInstance(User, userJson);
console.log(user.getFullName()); // Tom Smith
console.log(user.firstName); // Tomclass-transformer 使用示例:
import { plainToInstance, instanceToPlain, Expose, Exclude, Type } from 'class-transformer';
export class UserDto {
@Expose()
id: number;
@Expose()
firstName: string;
@Expose()
lastName: string;
@Exclude()
password: string; // 不暴露密码字段
@Expose()
@Type(() => Date)
createdAt: Date;
}
// 普通对象 → Class 实例
const plainUser = {
id: 1,
firstName: 'Tom',
lastName: 'Smith',
password: '123456',
createdAt: '2024-01-01T00:00:00.000Z',
};
const userInstance = plainToInstance(UserDto, plainUser);
// userInstance 是 UserDto 的实例
// userInstance.password 会被排除
// Class 实例 → 普通对象
const plainObject = instanceToPlain(userInstance);
// plainObject 是普通对象
// plainObject 中不包含 password 字段3.3 两个库的关系
class-validator 和 class-transformer 的关系:
│
├── class-transformer
│ ├── 负责数据转换
│ ├── JSON 对象 → Class 实例
│ ├── 控制序列化/反序列化
│ └── 为 class-validator 准备数据
│
├── class-validator
│ ├── 负责数据校验
│ ├── 校验 Class 实例的属性
│ ├── 提供丰富的校验装饰器
│ └── 返回校验结果或错误
│
└── ValidationPipe
├── 协调两个库的工作
├── 自动调用 class-transformer 转换
├── 自动调用 class-validator 校验
└── 统一处理校验结果工作流程:
ValidationPipe 工作流程:
│
├── 第一步:接收请求数据
│ └── { "name": "Tom", "age": 25, "email": "tom@example.com" }
│
├── 第二步:class-transformer 转换
│ ├── 将 JSON 对象转换为 DTO 类实例
│ └── CreateUserDto 实例
│
├── 第三步:class-validator 校验
│ ├── 校验 DTO 实例的属性
│ ├── 检查装饰器规则
│ └── 返回校验结果
│
└── 第四步:结果处理
├── 校验成功 → 进入 Controller
└── 校验失败 → 抛出异常,返回错误信息四、ValidationPipe 工作原理
4.1 管道的两大类型
NestJS Pipe 的两大类型:
│
├── 1. 转换类型 Pipe(Transformation)
│ ├── 作用:将输入数据转换为所需格式
│ ├── 示例:ParseIntPipe、ParseBoolPipe
│ ├── 场景:类型转换、格式转换
│ └── 流程:string → number
│
└── 2. 校验类型 Pipe(Validation)
├── 作用:验证输入数据是否符合要求
├── 示例:ValidationPipe、ParseUUIDPipe
├── 场景:数据校验、格式验证
└── 流程:数据 → 校验 → 通过/失败4.2 ValidationPipe 完整工作流程
ValidationPipe 完整工作流程图:
│
├── 第一步:用户发起请求
│ └── POST /users { "name": "", "age": -1, "email": "invalid" }
│
├── 第二步:请求到达 Controller 前
│ └── 经过 ValidationPipe
│
├── 第三步:ValidationPipe 调用 class-transformer
│ ├── 将 JSON 对象转换为 DTO 类实例
│ └── const dto = plainToInstance(CreateUserDto, requestBody)
│
├── 第四步:ValidationPipe 调用 class-validator
│ ├── 校验 DTO 实例的所有属性
│ ├── 检查每个属性的装饰器规则
│ └── const errors = await validate(dto)
│
├── 第五步:判断校验结果
│ ├── errors.length === 0 → 校验成功
│ └── errors.length > 0 → 校验失败
│
├── 第六步 A:校验成功
│ ├── 将 DTO 实例传递给 Controller
│ ├── Controller 调用 Service
│ ├── Service 执行业务逻辑
│ └── 返回响应给前端
│
└── 第六步 B:校验失败
├── 抛出 BadRequestException
├── 不进入 Controller
├── 不执行后续业务逻辑
└── 返回详细的错误信息给前端4.3 ValidationPipe 工作原理详解
请求流程对比:
有 ValidationPipe:
│
用户请求 → ValidationPipe → 校验成功 → Controller → Service → Database
↓
校验失败 → 返回错误(不进入 Controller)
无 ValidationPipe:
│
用户请求 → Controller → Service → Database
↓
可能写入无效数据ValidationPipe 的优势:
ValidationPipe 的优势:
│
├── 1. 提前拦截
│ ├── 校验失败不进入 Controller
│ ├── 不执行业务逻辑
│ └── 减少服务器资源消耗
│
├── 2. 减少数据库压力
│ ├── 无效数据不进入 Service
│ ├── 无效数据不查询数据库
│ └── 避免无效的数据库操作
│
├── 3. 统一的错误格式
│ ├── 自动抛出 BadRequestException
│ ├── 返回详细的错误信息
│ └── 前端易于处理错误
│
└── 4. 代码简洁
├── 使用装饰器定义规则
├── 无需手动编写校验逻辑
└── 易于维护和扩展4.4 校验失败的处理
校验失败时的行为:
// 用户提交的数据
{
"name": "", // 不能为空
"age": -1, // 不能小于 0
"email": "invalid" // 不是有效的邮箱格式
}
// ValidationPipe 校验失败后的响应
{
"statusCode": 400,
"message": [
"name 不能为空",
"age 不能小于 0",
"email 格式不正确"
],
"error": "Bad Request"
}重要特性:
- 校验失败会立即返回错误
- 不会进入 Controller 方法
- 不会执行后续业务逻辑
- 不会访问数据库
五、class-validator 常用装饰器
5.1 类型校验装饰器
| 装饰器 | 作用 | 示例 |
|---|---|---|
@IsString() | 校验字符串 | @IsString() |
@IsNumber() | 校验数字 | @IsNumber() |
@IsInt() | 校验整数 | @IsInt() |
@IsBoolean() | 校验布尔值 | @IsBoolean() |
@IsDate() | 校验日期 | @IsDate() |
@IsArray() | 校验数组 | @IsArray() |
@IsEnum() | 校验枚举 | @IsEnum(Status) |
@IsObject() | 校验对象 | @IsObject() |
5.2 字符串校验装饰器
| 装饰器 | 作用 | 示例 |
|---|---|---|
@IsEmail() | 校验邮箱格式 | @IsEmail() |
@IsUrl() | 校验 URL 格式 | @IsUrl() |
@IsUUID() | 校验 UUID 格式 | @IsUUID() |
@IsDate() | 校验日期格式 | @IsDateString() |
@IsIP() | 校验 IP 地址 | @IsIP() |
@IsJSON() | 校验 JSON 字符串 | @IsJSON() |
@IsMongoId() | 校验 MongoDB ID | @IsMongoId() |
5.3 长度和范围校验装饰器
| 装饰器 | 作用 | 示例 |
|---|---|---|
@MinLength(min) | 最小长度 | @MinLength(6) |
@MaxLength(max) | 最大长度 | @MaxLength(20) |
@Length(min, max) | 固定长度范围 | @Length(6, 20) |
@Min(min) | 最小值 | @Min(0) |
@Max(max) | 最大值 | @Max(150) |
@Range(min, max) | 数值范围 | @Range(0, 150) |
5.4 必填和可选装饰器
| 装饰器 | 作用 | 示例 |
|---|---|---|
@IsNotEmpty() | 非空校验 | @IsNotEmpty() |
@IsOptional() | 可选字段 | @IsOptional() |
@IsEmpty() | 必须为空 | @IsEmpty() |
@IsDefined() | 必须定义 | @IsDefined() |
5.5 相等性校验装饰器
| 装饰器 | 作用 | 示例 |
|---|---|---|
@Equals(value) | 等于某个值 | @Equals('admin') |
@NotEquals(value) | 不等于某个值 | @NotEquals('guest') |
@IsIn(values) | 在列表中 | @IsIn(['admin', 'user']) |
@IsNotIn(values) | 不在列表中 | @IsNotIn(['guest', 'banned']) |
5.6 常用装饰器示例
import {
IsString,
IsInt,
IsEmail,
IsNotEmpty,
IsOptional,
Min,
Max,
MinLength,
MaxLength,
IsEnum,
IsArray,
IsDate,
} from 'class-validator';
export class CreateUserDto {
// 字符串校验
@IsString({ message: 'name 必须是字符串' })
@IsNotEmpty({ message: 'name 不能为空' })
@MinLength(2, { message: 'name 至少 2 个字符' })
@MaxLength(20, { message: 'name 最多 20 个字符' })
name: string;
// 数字校验
@IsInt({ message: 'age 必须是整数' })
@Min(0, { message: 'age 不能小于 0' })
@Max(150, { message: 'age 不能大于 150' })
age: number;
// 邮箱校验
@IsEmail({}, { message: 'email 格式不正确' })
@IsNotEmpty({ message: 'email 不能为空' })
email: string;
// 可选字段
@IsOptional()
@IsString()
@MaxLength(200)
bio?: string;
// 枚举校验
@IsEnum(['admin', 'user', 'guest'], { message: 'role 必须是 admin、user 或 guest' })
role: string;
// 数组校验
@IsArray({ message: 'tags 必须是数组' })
@IsString({ each: true, message: 'tags 中的每个元素必须是字符串' })
tags: string[];
// 日期校验
@IsDate({ message: 'birthday 必须是日期' })
@IsOptional()
birthday?: Date;
}六、自定义校验装饰器
6.1 自定义校验器
创建自定义校验器:
import {
ValidatorConstraint,
ValidatorConstraintInterface,
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
// 1. 定义校验器
@ValidatorConstraint({ name: 'isLongerThan', async: false })
export class IsLongerThanConstraint implements ValidatorConstraintInterface {
validate(propertyValue: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return (
typeof propertyValue === 'string' &&
typeof relatedValue === 'string' &&
propertyValue.length > relatedValue.length
);
}
defaultMessage(args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
return `${args.property} 必须比 ${relatedPropertyName} 长`;
}
}
// 2. 定义装饰器
export function IsLongerThan(
property: string,
validationOptions?: ValidationOptions,
) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [property],
validator: IsLongerThanConstraint,
});
};
}使用自定义装饰器:
export class CreateUserDto {
@IsString()
firstName: string;
@IsString()
@IsLongerThan('firstName', { message: 'lastName 必须比 firstName 长' })
lastName: string;
}6.2 常见自定义校验示例
示例一:手机号校验:
import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
@ValidatorConstraint({ name: 'isPhone', async: false })
export class IsPhoneConstraint implements ValidatorConstraintInterface {
validate(phone: any) {
if (typeof phone !== 'string') return false;
// 中国大陆手机号正则
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(phone);
}
defaultMessage() {
return '手机号格式不正确';
}
}
// 使用装饰器
export function IsPhone(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: IsPhoneConstraint,
});
};
}示例二:密码强度校验:
@ValidatorConstraint({ name: 'isStrongPassword', async: false })
export class IsStrongPasswordConstraint implements ValidatorConstraintInterface {
validate(password: any) {
if (typeof password !== 'string') return false;
// 至少 8 位,包含大小写字母、数字和特殊字符
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
return regex.test(password);
}
defaultMessage() {
return '密码至少 8 位,必须包含大小写字母、数字和特殊字符';
}
}七、ValidationPipe 配置选项
7.1 全局配置 ValidationPipe
在 main.ts 中配置:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局配置 ValidationPipe
app.useGlobalPipes(
new ValidationPipe({
// 选项配置
}),
);
await app.listen(3000);
}
bootstrap();7.2 ValidationPipe 配置选项详解
app.useGlobalPipes(
new ValidationPipe({
// 1. 转换选项
transform: true, // 自动转换类型(推荐)
transformOptions: {
enableImplicitConversion: true, // 启用隐式类型转换
excludeExtraneousValues: true, // 排除未使用 @Expose() 装饰的属性
},
// 2. 校验选项
disableErrorMessages: false, // 是否禁用详细错误信息(生产环境可设为 true)
validationError: {
target: false, // 不在错误中包含目标对象
value: false, // 不在错误中包含错误值
},
// 3. 白名单选项
whitelist: true, // 启用白名单(推荐)
forbidNonWhitelisted: true, // 拒绝未在白名单中的属性(推荐)
// 4. 跳过属性
skipMissingProperties: false, // 是否跳过缺失的属性
skipNullProperties: false, // 是否跳过 null 属性
skipUndefinedProperties: false, // 是否跳过 undefined 属性
// 5. 分组校验
always: false, // 始终校验,不管分组
groups: [], // 校验分组
// 6. 停止校验
stopAtFirstError: false, // 遇到第一个错误就停止(推荐)
}),
);7.3 关键配置说明
1. transform: true(自动类型转换):
// DTO 定义
export class CreateUserDto {
@IsInt()
age: number;
}
// 用户提交的数据
{
"age": "25" // string 类型
}
// transform: false → age 是 "25"(string)
// transform: true → age 是 25(number)2. whitelist: true + forbidNonWhitelisted: true(白名单机制):
// DTO 定义
export class CreateUserDto {
@IsString()
name: string;
@IsInt()
age: number;
}
// 用户提交的数据(包含恶意字段)
{
"name": "Tom",
"age": 25,
"isAdmin": true, // 恶意字段
"role": "admin" // 恶意字段
}
// whitelist: true, forbidNonWhitelisted: true
// 结果:抛出错误,拒绝包含未定义字段的请求
// whitelist: true, forbidNonWhitelisted: false
// 结果:自动过滤掉 isAdmin 和 role 字段
// whitelist: false
// 结果:保留所有字段(包括恶意字段)3. stopAtFirstError: true(遇到第一个错误就停止):
// 用户提交的数据
{
"name": "",
"age": -1,
"email": "invalid"
}
// stopAtFirstError: false
// 返回所有错误:
// ["name 不能为空", "age 不能小于 0", "email 格式不正确"]
// stopAtFirstError: true
// 返回第一个错误:
// ["name 不能为空"]7.4 生产环境推荐配置
// 开发环境配置
const devValidationOptions = {
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
stopAtFirstError: false,
disableErrorMessages: false, // 显示详细错误信息
};
// 生产环境配置
const prodValidationOptions = {
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
stopAtFirstError: true,
disableErrorMessages: true, // 隐藏详细错误信息(安全)
};
// 根据环境选择配置
app.useGlobalPipes(
new ValidationPipe(
process.env.NODE_ENV === 'production'
? prodValidationOptions
: devValidationOptions
),
);八、完整实战示例
8.1 安装依赖
# 安装 class-validator 和 class-transformer
$ pnpm add class-validator class-transformer8.2 创建 DTO
CreateUserDto:
// src/modules/user/dto/create-user.dto.ts
import {
IsString,
IsInt,
IsEmail,
IsNotEmpty,
IsOptional,
Min,
Max,
MinLength,
MaxLength,
IsEnum,
IsArray,
IsDateString,
IsPhoneNumber,
} from 'class-validator';
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest',
}
export class CreateUserDto {
// 必填字段
@IsString({ message: '用户名必须是字符串' })
@IsNotEmpty({ message: '用户名不能为空' })
@MinLength(2, { message: '用户名至少 2 个字符' })
@MaxLength(20, { message: '用户名最多 20 个字符' })
username: string;
@IsString({ message: '密码必须是字符串' })
@IsNotEmpty({ message: '密码不能为空' })
@MinLength(8, { message: '密码至少 8 个字符' })
@MaxLength(20, { message: '密码最多 20 个字符' })
password: string;
@IsEmail({}, { message: '邮箱格式不正确' })
@IsNotEmpty({ message: '邮箱不能为空' })
email: string;
// 可选字段
@IsOptional()
@IsString({ message: '昵称必须是字符串' })
@MaxLength(50, { message: '昵称最多 50 个字符' })
nickname?: string;
@IsOptional()
@IsInt({ message: '年龄必须是整数' })
@Min(0, { message: '年龄不能小于 0' })
@Max(150, { message: '年龄不能大于 150' })
age?: number;
@IsOptional()
@IsEnum(UserRole, { message: '角色必须是 admin、user 或 guest' })
role?: UserRole;
@IsOptional()
@IsArray({ message: '标签必须是数组' })
@IsString({ each: true, message: '标签中的每个元素必须是字符串' })
tags?: string[];
@IsOptional()
@IsDateString({}, { message: '生日格式不正确' })
birthday?: string;
}8.3 配置全局 ValidationPipe
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 配置全局 ValidationPipe
app.useGlobalPipes(
new ValidationPipe({
transform: true, // 自动类型转换
whitelist: true, // 启用白名单
forbidNonWhitelisted: true, // 拒绝未定义的字段
stopAtFirstError: false, // 返回所有错误
disableErrorMessages: false, // 显示详细错误信息
validationError: {
target: false,
value: false,
},
}),
);
await app.listen(3000);
}
bootstrap();8.4 Controller 使用 DTO
// src/modules/user/user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
async create(@Body() createUserDto: CreateUserDto) {
// createUserDto 已经经过校验,数据是安全的
return this.userService.create(createUserDto);
}
}8.5 测试示例
测试一:校验成功:
# 请求
POST http://localhost:3000/users
Content-Type: application/json
{
"username": "tomsmith",
"password": "password123",
"email": "tom@example.com",
"age": 25,
"role": "user",
"tags": ["developer", "designer"]
}
# 响应
{
"id": 1,
"username": "tomsmith",
"email": "tom@example.com",
"age": 25,
"role": "user",
"tags": ["developer", "designer"]
}测试二:校验失败(字段为空):
# 请求
POST http://localhost:3000/users
Content-Type: application/json
{
"username": "",
"password": "123",
"email": "invalid-email"
}
# 响应
{
"statusCode": 400,
"message": [
"用户名不能为空",
"用户名至少 2 个字符",
"密码至少 8 个字符",
"邮箱格式不正确"
],
"error": "Bad Request"
}测试三:校验失败(包含恶意字段):
# 请求
POST http://localhost:3000/users
Content-Type: application/json
{
"username": "tomsmith",
"password": "password123",
"email": "tom@example.com",
"isAdmin": true, // 恶意字段
"role": "admin" // 恶意字段
}
# 响应(forbidNonWhitelisted: true)
{
"statusCode": 400,
"message": [
"property isAdmin should not exist",
"property role should not exist"
],
"error": "Bad Request"
}九、校验管道的最佳实践
9.1 DTO 设计原则
DTO 设计原则:
│
├── 1. 单一职责
│ ├── CreateUserDto:创建用户
│ ├── UpdateUserDto:更新用户
│ └── QueryUserDto:查询用户
│
├── 2. 使用 PartialType
│ ├── UpdateUserDto extends PartialType(CreateUserDto)
│ └── 所有字段变为可选
│
├── 3. 分组校验
│ ├── groups: ['create'] - 创建时校验
│ ├── groups: ['update'] - 更新时校验
│ └── 不同场景不同校验规则
│
└── 4. 继承复用
├── BaseDto:公共字段
└── CreateUserDto extends BaseDto9.2 错误消息国际化
// 使用函数动态返回错误消息
@IsString({
message: (args: ValidationArguments) => {
return `${args.property} 必须是字符串`;
},
})
name: string;9.3 嵌套对象校验
import { ValidateNested, IsString, Type } from 'class-validator';
export class AddressDto {
@IsString()
city: string;
@IsString()
street: string;
}
export class CreateUserDto {
@IsString()
name: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}9.4 异步校验
import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
import { UserService } from './user.service';
@ValidatorConstraint({ name: 'isUserAlreadyExist', async: true })
export class IsUserAlreadyExistConstraint implements ValidatorConstraintInterface {
constructor(private userService: UserService) {}
async validate(email: string) {
const user = await this.userService.findByEmail(email);
return !user; // 用户不存在时返回 true
}
defaultMessage() {
return '用户已存在';
}
}十、常见问题与解决方案
10.1 校验不生效
问题:配置了 ValidationPipe,但校验不生效。
原因:
- 没有全局配置 ValidationPipe
- DTO 中没有使用校验装饰器
- 没有安装 class-validator 和 class-transformer
解决方案:
// 1. 确保安装了依赖
$ pnpm add class-validator class-transformer
// 2. 在 main.ts 中全局配置
app.useGlobalPipes(new ValidationPipe());
// 3. 在 DTO 中使用装饰器
export class CreateUserDto {
@IsString()
name: string;
}10.2 类型转换失败
问题:提交的数据类型没有自动转换。
原因:没有开启 transform: true。
解决方案:
app.useGlobalPipes(
new ValidationPipe({
transform: true, // 启用自动类型转换
}),
);10.3 恶意字段未过滤
问题:提交的恶意字段(如 isAdmin)没有被过滤。
原因:没有开启白名单机制。
解决方案:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 启用白名单
forbidNonWhitelisted: true, // 拒绝未定义的字段
}),
);10.4 错误信息过于详细
问题:生产环境不想暴露详细的错误信息。
原因:disableErrorMessages 为 false。
解决方案:
// 生产环境配置
app.useGlobalPipes(
new ValidationPipe({
disableErrorMessages: true, // 禁用详细错误信息
}),
);十一、学习要点总结
11.1 核心知识点
本节核心知识点:
│
├── 数据校验的重要性
│ ├── 前端校验 + 后端校验
│ ├── 后端校验是必须的
│ └── 保障数据安全和完整性
│
├── ValidationPipe 工作原理
│ ├── 用户请求 → ValidationPipe
│ ├── class-transformer 转换数据
│ ├── class-validator 校验数据
│ ├── 校验成功 → Controller
│ └── 校验失败 → 返回错误
│
├── class-validator
│ ├── 基于装饰器的类型校验
│ ├── 提供丰富的内置装饰器
│ ├── 支持自定义校验器
│ └── 校验 Class 实例的属性
│
├── class-transformer
│ ├── 基于装饰器的对象转换
│ ├── JSON 对象 → Class 实例
│ ├── 控制序列化/反序列化
│ └── 为 class-validator 准备数据
│
├── ValidationPipe 配置
│ ├── transform: true(类型转换)
│ ├── whitelist: true(白名单)
│ ├── forbidNonWhitelisted: true(拒绝恶意字段)
│ └── stopAtFirstError: true(遇到第一个错误停止)
│
└── 最佳实践
├── DTO 单一职责
├── 使用 PartialType 复用
├── 嵌套对象校验
└── 分组校验11.2 学习路径规划
学习路径:
│
├── 第一阶段:理解概念(1 天)
│ ├── 理解数据校验的重要性
│ ├── 理解 ValidationPipe 工作原理
│ └── 理解 class-validator 和 class-transformer 的作用
│
├── 第二阶段:实践使用(2-3 天)
│ ├── 安装依赖
│ ├── 创建 DTO
│ ├── 配置全局 ValidationPipe
│ └── 测试校验功能
│
└── 第三阶段:深入应用(持续)
├── 自定义校验装饰器
├── 嵌套对象校验
├── 分组校验
└── 异步校验11.3 重要程度标注
重要程度说明:
│
├── 必须掌握
│ ├── 数据校验的重要性
│ ├── ValidationPipe 工作原理
│ ├── class-validator 常用装饰器
│ ├── ValidationPipe 关键配置
│ └── 创建 DTO 并使用装饰器
│
├── 重要
│ ├── class-transformer 的作用
│ ├── ValidationPipe 配置选项
│ ├── 白名单机制
│ └── 类型自动转换
│
└── 了解
├── 自定义校验装饰器
├── 嵌套对象校验
├── 分组校验
└── 异步校验重要提示:这一节是 NestJS 数据校验的核心内容,理解 ValidationPipe 工作原理、熟练使用 class-validator 装饰器、掌握 ValidationPipe 配置,对实际项目开发非常重要!特别是要理解校验管道的工作原理,以及如何通过白名单机制防止恶意数据注入!
下一节预告:下一节将学习完整的 CRUD 接口实现,包括创建、更新、删除等操作的数据校验和业务逻辑处理。