TypeScript 给了我们极好的开发体验——IDE 提示、类型推断、编译期报错。但有一个盲区很容易被忽略:TypeScript 的类型在运行时是不存在的。
你声明了 name: string,但如果客户端传来 name: 123,TypeScript 不会拦截,代码会默默地用一个数字当字符串处理,直到某个地方出错才发现。
这就是 class-validator 要解决的问题:在运行时真正验证数据的形状和内容。
两层保护:编译时 vs 运行时
客户端请求 ──► [运行时校验] ──► [TypeScript 类型系统] ──► 业务逻辑
class-validator 编译期类型检查
(真正拦截非法数据) (IDE 提示 + 类型推断)
两者不是替代关系,而是互补:TypeScript 保证代码内部的类型安全,class-validator 保证外部输入符合预期。
核心概念:DTO
DTO(Data Transfer Object,数据传输对象)是专门描述"接口接收/返回什么数据"的类。在 NestJS 中,DTO 是 class-validator 的天然载体:
// create-user.dto.ts
import { IsString, IsEmail, IsInt, Min, Max, IsOptional } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
@MaxLength(50)
name: string;
@IsEmail()
email: string;
@IsInt()
@Min(18)
@Max(120)
age: number;
@IsOptional()
@IsString()
department?: string;
}
装饰器即文档:读这个 DTO,不用看任何注释就知道接口要什么数据、有什么限制。
在 NestJS 中启用校验管道
NestJS 通过 ValidationPipe 把 class-validator 接入请求生命周期。推荐全局开启:
// main.ts
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 自动剥离 DTO 未声明的字段
forbidNonWhitelisted: true, // 有未知字段时直接报错(更严格)
transform: true, // 自动将原始数据转换为 DTO 类实例
}),
);
这三个选项值得关注:
whitelist: true:客户端传了多余字段,管道会自动过滤,避免意外写入数据库forbidNonWhitelisted: true:如果你想知道客户端是否在"试探"接口,可以开启这个transform: true:URL 参数默认是字符串,开启后会自动转换为 DTO 中声明的类型
常用装饰器速查
// 字符串
@IsString() // 必须是字符串
@MinLength(2) // 最短长度
@MaxLength(100) // 最长长度
@Matches(/^[a-z]+$/) // 正则匹配
// 数字
@IsInt() // 必须是整数
@IsNumber() // 必须是数字(含浮点)
@Min(0) // 最小值
@Max(100) // 最大值
// 布尔 / 枚举
@IsBoolean()
@IsEnum(UserRole) // 必须是枚举值之一
// 格式
@IsEmail()
@IsUrl()
@IsDate()
@IsISO8601() // ISO 日期格式
// 存在性
@IsOptional() // 字段可缺省(缺省时跳过其他校验)
@IsNotEmpty() // 不能为空字符串/null/undefined
// 数组
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(10)
嵌套对象校验
当 DTO 包含嵌套对象时,需要配合 @ValidateNested() 和 @Type():
import { Type } from 'class-transformer';
export class AddressDto {
@IsString()
city: string;
@IsString()
street: string;
}
export class CreateCompanyDto {
@IsString()
name: string;
@ValidateNested()
@Type(() => AddressDto) // class-transformer 负责实例化嵌套对象
address: AddressDto;
}
@Type() 来自 class-transformer,它告诉管道把原始 JSON 对象实例化为对应的类,class-validator 才能识别并校验。
自定义校验器
内置装饰器不够用时,可以写自定义校验逻辑:
import { registerDecorator, ValidationOptions } from 'class-validator';
export function IsChinesePhone(validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isChinesePhone',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate(value: any) {
return /^1[3-9]\d{9}$/.test(value);
},
defaultMessage() {
return '请输入有效的手机号码';
},
},
});
};
}
// 使用
export class ContactDto {
@IsChinesePhone()
phone: string;
}
自定义校验器同样支持异步逻辑(比如检查数据库里是否已存在某个值),只需让 validate 方法返回 Promise<boolean>。
错误信息的格式
校验失败时,NestJS 默认返回结构化的错误响应,便于前端处理:
{
"statusCode": 400,
"message": [
"name must be longer than or equal to 2 characters",
"email must be an email"
],
"error": "Bad Request"
}
可以通过 exceptionFactory 自定义错误格式,比如按字段名分组。
分离 Create / Update DTO
创建和更新操作对字段的要求往往不同:创建时所有字段必填,更新时大多数字段是可选的。可以用继承 + PartialType 避免重复:
import { PartialType } from '@nestjs/mapped-types';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
}
// UpdateUserDto 中所有字段自动变为可选
export class UpdateUserDto extends PartialType(CreateUserDto) {}
PartialType 是 NestJS 提供的工具,会把父类所有字段包裹为 @IsOptional(),同时保留原有的校验规则。
小结
class-validator 让 TypeScript 项目的数据安全延伸到了运行时:
| 场景 | 用法 |
|---|---|
| 基础字段校验 | 内置装饰器(@IsString、@IsEmail 等) |
| 嵌套对象 | @ValidateNested() + @Type() |
| 业务规则 | 自定义校验器 |
| 创建/更新复用 | PartialType 继承 |
| 全局启用 | ValidationPipe + transform: true |
我们在实际项目中推行了"所有外部输入都必须经过 DTO 校验"的规范,Bug 排查效率明显提升——因为非法数据会在入口就被拦截,而不是在业务代码深处才出错。
作者:ekent · ek Studio 祎坤