数据验证
本教程共 47 篇 · 第 25 篇 · 更新于 2026-08-09 · 约 11 分钟阅读
本节目标:掌握 NestJS 中的数据验证机制,学会用 ValidationPipe + class-validator 自动校验请求数据,用内置 Pipe 做类型转换,写出健壮的接口。
永远不要相信客户端传来的数据。
用户可能输错邮箱格式,可能传一个字符串到你期望是数字的地方,甚至可能故意塞一堆多余字段。如果你的接口不做验证,轻则数据脏乱,重则被注入攻击。
NestJS 的验证机制很优雅——写好 DTO 类,贴上验证装饰器,框架自动帮你校验。不合规的数据直接被拦下来,返回 400 错误。
安装依赖
NestJS 的数据验证依赖两个库:
class-validator:提供验证装饰器class-transformer:提供对象转换能力
npm install class-validator class-transformer
第一个 DTO
DTO(Data Transfer Object)就是数据传输对象。它定义了客户端应该传什么格式的数据。
// dto/create-user.dto.ts
import { IsEmail, IsNotEmpty, IsString, MinLength, MaxLength } from 'class-validator';
export class CreateUserDto {
@IsEmail({}, { message: '邮箱格式不正确' })
@IsNotEmpty({ message: '邮箱不能为空' })
email: string;
@IsString()
@MinLength(6, { message: '密码至少 6 个字符' })
@MaxLength(32, { message: '密码最多 32 个字符' })
password: string;
@IsString()
@IsNotEmpty({ message: '用户名不能为空' })
name: string;
}
每个装饰器就是一条验证规则。@IsEmail() 检查是不是合法邮箱,@MinLength(6) 检查最小长度。一个字段可以贴多个装饰器,所有规则都必须通过才算验证成功。
启用 ValidationPipe
光定义 DTO 还不够,你得告诉 NestJS 去验证。方法是使用 ValidationPipe。
全局启用(推荐)
// main.ts
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 所有接口都自动验证
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
在控制器方法上启用
如果只想验证某个接口:
@Post()
@UsePipes(new ValidationPipe())
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
Tip建议全局启用。每个接口单独绑太麻烦,而且容易漏掉。
验证失败会怎样?
当客户端传来的数据不符合 DTO 中定义的规则时,NestJS 自动返回 400 响应:
{
"statusCode": 400,
"message": [
"邮箱格式不正确",
"密码至少 6 个字符"
],
"error": "Bad Request"
}
不需要你写任何错误处理逻辑,框架全包了。
常用验证装饰器
class-validator 提供了大量装饰器,这里列出最常用的:
字符串验证
@IsString() // 必须是字符串
@IsEmail() // 必须是邮箱格式
@IsUrl() // 必须是 URL
@IsUUID() // 必须是 UUID
@MinLength(3) // 最小长度
@MaxLength(100) // 最大长度
@Matches(/^[a-zA-Z]+$/) // 正则匹配
数字验证
@IsNumber() // 必须是数字
@IsInt() // 必须是整数
@Min(0) // 最小值
@Max(999) // 最大值
@IsPositive() // 必须是正数
通用验证
@IsNotEmpty() // 不能为空
@IsOptional() // 可选字段(如果不存在则跳过验证)
@IsBoolean() // 必须是布尔值
@IsArray() // 必须是数组
@IsEnum(['a', 'b']) // 必须是枚举值之一
@IsDateString() // 必须是日期字符串
一个完整的 DTO 示例
import {
IsString, IsEmail, IsNotEmpty, IsOptional,
MinLength, MaxLength, IsInt, Min, Max,
IsEnum, IsArray, ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
export class CreatePostDto {
@IsString()
@IsNotEmpty({ message: '标题不能为空' })
@MaxLength(200, { message: '标题不能超过 200 字' })
title: string;
@IsString()
@MinLength(10, { message: '正文至少 10 个字符' })
content: string;
@IsEnum(['draft', 'published', 'archived'], {
message: '状态必须是 draft、published 或 archived',
})
status: string;
@IsArray()
@IsString({ each: true }) // 数组中每个元素都必须是字符串
@IsOptional()
tags?: string[];
@IsInt()
@Min(1)
@Max(100)
@IsOptional()
sortOrder?: number;
}
Note
@IsOptional()表示这个字段可以不传。如果不加这个装饰器,字段就是必填的。注意:@IsOptional()只检查”字段不存在”的情况。如果传了null或空字符串,它还是会触发其他验证规则。
验证嵌套对象
如果 DTO 里包含嵌套对象,需要加 @ValidateNested() 装饰器,并配合 @Type() 使用:
import { Type } from 'class-transformer';
export class AddressDto {
@IsString()
@IsNotEmpty()
city: string;
@IsString()
@IsNotEmpty()
street: string;
}
export class CreateUserDto {
@IsString()
name: string;
@ValidateNested() // 告诉验证器:这是个嵌套对象,要去验证它
@Type(() => AddressDto) // 告诉转换器:用 AddressDto 来实例化
address: AddressDto;
}
Warning不加
@Type(() => AddressDto)的话,address只是一个普通对象,不是AddressDto的实例,验证规则不会生效。这是新手常踩的坑。
自动类型转换
HTTP 请求传来的数据都是字符串。URL 参数 ?id=123 里的 123 是字符串 "123",不是数字 123。
ValidationPipe 可以自动做类型转换。开启 transform 选项:
app.useGlobalPipes(new ValidationPipe({
transform: true,
}));
开启后,如果你的控制器方法声明了 id: number,ValidationPipe 会自动把字符串 "123" 转成数字 123:
@Get(':id')
findOne(@Param('id') id: number) {
console.log(typeof id); // 'number',自动转换了
}
Tip
transform: true还会把请求体自动转成 DTO 类的实例。这意味着 DTO 上的方法(如果有的话)也能正常使用。
内置 Pipe
除了 ValidationPipe,NestJS 还提供了几个内置 Pipe 做简单的类型转换:
ParseIntPipe
把字符串转成整数。转换失败返回 400。
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// id 一定是 number 类型
}
ParseBoolPipe
把字符串转成布尔值。"true" 变 true,"false" 变 false。
@Get()
findAll(@Query('active', ParseBoolPipe) active: boolean) {
// active 是 boolean
}
ParseArrayPipe
验证和解析数组。适合批量操作或逗号分隔的查询参数:
@Post()
createBulk(
@Body(new ParseArrayPipe({ items: CreateUserDto }))
users: CreateUserDto[],
) {
return this.userService.createBulk(users);
}
@Get()
findByIds(
@Query('ids', new ParseArrayPipe({ items: Number, separator: ',' }))
ids: number[],
) {
// GET /users?ids=1,2,3 -> ids = [1, 2, 3]
return this.userService.findByIds(ids);
}
ParseUUIDPipe
验证 UUID 格式:
@Get(':id')
findOne(@Param('id', new ParseUUIDPipe()) id: string) {
// id 一定是 UUID 格式
}
白名单:过滤多余字段
客户端可能传一些你不需要甚至不该接收的字段。比如你的 DTO 只有 email 和 password,但客户端多传了一个 role: 'admin'。
whitelist: true 会自动去掉 DTO 中没有声明的字段:
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
}));
更进一步,forbidNonWhitelisted: true 不仅去掉多余字段,还直接报错:
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
}));
这时候如果客户端传了多余字段,会收到 400 错误。
Tip生产环境建议开启
whitelist: true。这是一个很好的安全习惯——只接收你需要的数据,多余的直接丢掉。
映射类型:减少重复代码
做 CRUD 的时候,Create DTO 和 Update DTO 往往很像。区别是:创建时所有字段必填,更新时所有字段可选。
不用把每个字段重新写一遍。NestJS 提供了映射类型工具函数:
PartialType:所有字段变可选
import { PartialType } from '@nestjs/mapped-types';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
UpdateUserDto 继承了 CreateUserDto 的所有字段和验证规则,但每个字段都变成了可选的。
PickType:只取部分字段
import { PickType } from '@nestjs/mapped-types';
export class UpdatePasswordDto extends PickType(CreateUserDto, ['password'] as const) {}
只取 password 字段。
OmitType:排除部分字段
import { OmitType } from '@nestjs/mapped-types';
export class UpdateUserDto extends OmitType(CreateUserDto, ['email'] as const) {}
取除了 email 以外的所有字段。
IntersectionType:合并两个 DTO
import { IntersectionType } from '@nestjs/mapped-types';
export class CreateUserWithProfileDto extends IntersectionType(
CreateUserDto,
CreateProfileDto,
) {}
把两个 DTO 的字段合并到一起。
Note这些映射类型可以组合使用。比如”排除 email 字段,然后全部变可选”:
export class UpdateUserDto extends PartialType( OmitType(CreateUserDto, ['email'] as const), ) {}
自定义验证装饰器
class-validator 提供的装饰器不够用?你可以自定义验证规则。
方式一:用 @Validate 和 ValidatorConstraint
import {
ValidatorConstraint,
ValidatorConstraintInterface,
Validate,
} from 'class-validator';
// 定义验证逻辑
@ValidatorConstraint({ async: true })
export class IsEmailAlreadyExist implements ValidatorConstraintInterface {
constructor(private userService: UserService) {}
async validate(email: string): Promise<boolean> {
const user = await this.userService.findByEmail(email);
return !user; // 如果用户不存在,验证通过
}
defaultMessage(): string {
return '该邮箱已被注册';
}
}
// 在 DTO 中使用
export class CreateUserDto {
@Validate(IsEmailAlreadyExist)
email: string;
}
Warning自定义验证器如果用了依赖注入(比如上面的
UserService),需要通过useContainer设置容器,否则注入不生效。
方式二:自定义装饰器(更简洁)
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: string) {
// 至少包含一个大写字母、一个小写字母和一个数字
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(value);
},
defaultMessage() {
return '密码必须至少 8 位,包含大小写字母和数字';
},
},
});
};
}
// 使用
export class CreateUserDto {
@IsStrongPassword({ message: '密码强度不够' })
password: string;
}
自定义错误格式
默认的验证错误格式可能不符合你的 API 规范。可以自定义 exceptionFactory:
app.useGlobalPipes(new ValidationPipe({
exceptionFactory: (errors) => {
const formattedErrors = errors.reduce((acc, error) => {
acc[error.property] = Object.values(error.constraints || {});
return acc;
}, {} as Record<string, string[]>);
return {
statusCode: 400,
code: 'VALIDATION_ERROR',
errors: formattedErrors,
};
},
}));
这样返回的错误信息更结构化:
{
"statusCode": 400,
"code": "VALIDATION_ERROR",
"errors": {
"email": ["邮箱格式不正确"],
"password": ["密码至少 6 个字符"]
}
}
生产环境的注意事项
生产环境中,详细的验证错误信息可能暴露内部实现细节。可以关闭错误消息:
app.useGlobalPipes(new ValidationPipe({
disableErrorMessages: true,
}));
这样验证失败时只返回 400 状态码,不返回具体原因。
踩坑经验
1. DTO 必须用 class,不能用 interface
TypeScript 的 interface 在编译后会消失。运行时 NestJS 拿不到任何信息,验证自然不生效。必须用 class 定义 DTO。
// 错误:interface 编译后消失,验证不生效
interface CreateUserDto {
email: string;
}
// 正确:class 在运行时存在
class CreateUserDto {
@IsEmail()
email: string;
}
2. 导入 DTO 时不要用 type-only import
// 错误:type-only import 在运行时会被擦除
import type { CreateUserDto } from './dto/create-user.dto';
// 正确
import { CreateUserDto } from './dto/create-user.dto';
3. 嵌套对象必须加 @Type()
这是新手最常犯的错误。不加 @Type(() => ChildDto),嵌套对象不会被正确验证。
4. @IsOptional() 的位置
@IsOptional() 要放在其他验证装饰器前面:
// 正确
@IsOptional()
@IsString()
name?: string;
// 可能有问题
@IsString()
@IsOptional()
name?: string;
5. transform 和 whitelist 可以一起用
new ValidationPipe({
transform: true, // 自动类型转换
whitelist: true, // 过滤多余字段
forbidNonWhitelisted: true, // 多余字段直接报错
})
这三个选项配合使用,是生产环境的最佳实践。
小结
关键知识点回顾:
- DTO 必须用 class 定义,interface 编译后消失,验证不生效
ValidationPipe配合class-validator装饰器自动校验请求数据@Type(() => ChildDto)处理嵌套对象验证whitelist: true过滤多余字段,transform: true自动类型转换- 自定义验证装饰器用
createParamDecorator或registerDecorator
下一章我们聊聊数据库集成,从 TypeORM 开始。