返回博客列表
TypeScriptNestJS数据校验

TypeScript 运行时数据校验:class-validator 实战指南

TypeScript 类型系统只在编译期生效,class-validator 装饰器填补运行时空白,一份 DTO 同时做类型约束、参数校验和 API 文档。

作者: ekent·发布于 2026年3月1日

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 祎坤