16-4 接口参数校验:nestjs 基于装饰器的类验证器管道
一、类验证器核心工具
1.1 class-transformer 核心功能
// 将JSON对象转换为类实例
import { plainToClass } from 'class-transformer';
const user = plainToClass(User, jsonObject);
// 将类实例转换为普通对象
import { classToPlain } from 'class-transformer';
const plainUser = classToPlain(user);
// 序列化为JSON字符串
import { classToClass } from 'class-transformer';
const jsonString = JSON.stringify(classToClass(user));
typescript
核心功能
- 对象-类转换
- 将普通对象转换为类实例,支持嵌套对象转换。
- 常用于从数据库或API接收的数据转换为具有方法的类实例。
- 序列化与反序列化
- 支持将类实例序列化为JSON或普通对象,便于网络传输或存储。
- 反序列化时保留类方法和属性类型。
- 类型化支持
- 为
class-validator
提供类型化基础,确保校验时数据类型正确。
- 为
实践案例
假设从API获取用户数据:
const apiResponse = {
id: 1,
name: "Alice",
createdAt: "2023-10-01T00:00:00Z"
};
// 转换为User类实例
const user = plainToClass(User, apiResponse);
console.log(user.createdAt instanceof Date); // true
typescript
💡 提示:
- 使用
@Expose()
和@Exclude()
装饰器控制字段的序列化行为。 - 通过
@Type()
装饰器指定嵌套属性的类型,如@Type(() => Date)
。
1.2 class-validator 校验机制
import { IsString, Length, IsEmail, ValidateNested } from 'class-validator';
class AddressDTO {
@IsString()
city: string;
}
class UserDTO {
@IsString()
@Length(6, 20, { message: '用户名需6-20位字符' })
username: string;
@IsEmail()
email: string;
@ValidateNested()
address: AddressDTO;
}
typescript
核心功能
- 装饰器校验
- 通过装饰器声明校验规则,如
@IsString()
、@Length()
等。 - 支持属性级校验,灵活配置错误消息。
- 通过装饰器声明校验规则,如
- 内置校验规则
- 常见规则:
@IsBoolean()
、@IsDate()
、@IsIn(['admin', 'user'])
。 - 高级规则:
@Matches(/正则表达式/)
、@Min(18)
、@Max(100)
。
- 常见规则:
- 自定义校验
- 通过实现
ValidatorConstraintInterface
创建自定义装饰器。 - 支持异步校验(如数据库查重)。
- 通过实现
实践案例
自定义手机号校验器:
import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
@ValidatorConstraint({ name: 'isPhone' })
export class PhoneValidator implements ValidatorConstraintInterface {
validate(value: string) {
return /^1[3-9]\d{9}$/.test(value);
}
}
// 使用
class UserDTO {
@IsPhone({ message: '手机号格式错误' })
phone: string;
}
typescript
前沿动态
- NestJS 9+ 优化了校验管道的性能,支持更快的错误响应。
- class-validator 0.14+ 新增
@IsHash()
、@IsJWT()
等规则,适用于现代Web开发。
💡 提示:
- 使用
@ValidateIf()
实现条件校验,如仅当字段存在时校验。 - 通过
groups
参数分组校验,适用于多场景(如创建/更新)。
延伸学习资源
通过结合这两个工具,可以轻松实现强大的数据校验与转换功能! 🚀
二、验证管道配置实战
2.1 安装依赖库
# 安装核心校验库
pnpm add class-validator class-transformer
# 生产环境推荐锁定版本
pnpm add class-validator@0.14.0 class-transformer@0.5.1 --save-exact
bash
关键说明
- 版本管理策略
- 开发环境可使用
^
版本范围 - 生产环境务必使用
--save-exact
锁定版本 - 常见冲突版本:
# 已知兼容版本组合 class-validator@0.14.x + class-transformer@0.5.x
bash
- 开发环境可使用
- TypeScript配置要求
// tsconfig.json { "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } }
json
💡 遇到版本问题时:
- 查看NestJS兼容性矩阵
- 使用
npm ls class-validator
检查依赖树
2.2 全局管道配置
// main.ts
import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true, // 新增安全防护
transform: true, // 自动类型转换
disableErrorMessages: process.env.NODE_ENV === 'production' // 生产环境隐藏错误详情
})
);
typescript
2.2.1 whitelist 安全机制
选项名 | 类型 | 默认值 | 安全效果 |
---|---|---|---|
whitelist | boolean | false | 移除DTO未声明的字段 |
forbidNonWhitelisted | boolean | false | 发现非法字段时直接返回400错误(需whitelist=true) |
transform | boolean | false | 自动将输入数据转换为DTO类型(需配合class-transformer) |
transformOptions | object | {} | 配置转换行为,如enableImplicitConversion: true 启用隐式类型转换 |
实战场景对比
// 案例1:基础配置
new ValidationPipe({ whitelist: true })
// 案例2:严格模式配置
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
exceptionFactory: (errors) => new BadRequestException({
code: 40001,
message: '参数校验失败',
details: errors
})
})
typescript
安全防护原理
前沿实践
- 动态白名单:通过
groups
参数实现不同接口的字段过滤 - 性能优化:在高频接口禁用
transform
提升吞吐量 - 错误国际化:集成
class-validator-errors-extractor
实现多语言错误消息
💡 生产环境建议:
- 始终启用
whitelist
和forbidNonWhitelisted
- 通过
exceptionFactory
统一错误格式 - 使用
@ApiHideProperty()
配合Swagger隐藏敏感字段
扩展学习
三、DTO校验规则实现
3.1 创建用户登录DTO
// dto/sign-in-user.dto.ts
import { IsNotEmpty, IsString, Length, Matches, IsOptional } from 'class-validator';
export class SignInUserDto {
@IsNotEmpty({ message: '用户名不能为空' })
@IsString({ message: '用户名必须是字符串' })
@Length(6, 20, {
message: ({ min, max }) => `用户名长度需在${min}-${max}位之间`,
})
@Matches(/^[a-zA-Z0-9_]+$/, {
message: '用户名只能包含字母、数字和下划线'
})
username: string;
@IsNotEmpty({ message: '密码不能为空' })
@Length(6, 32, { message: '密码长度需6-32位' })
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, {
message: '密码必须包含大小写字母和数字'
})
password: string;
@IsOptional()
@IsString()
captcha?: string; // 可选字段示例
}
typescript
校验规则详解
- 基础校验
@IsNotEmpty()
:确保字段不为空@IsString()
:类型校验@Length()
:长度范围控制
- 高级校验
@Matches()
:正则表达式验证- 密码复杂度要求示例:
@Matches(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/, { message: '密码需包含大小写字母和数字' })
typescript
- 可选字段
@IsOptional()
:标记非必填字段- 配合
@IsString()
等使用
校验规则组合策略
校验目标 | 推荐规则组合 |
---|---|
用户名 | @IsNotEmpty + @IsString + @Length + @Matches (特殊字符限制) |
密码 | @IsNotEmpty + @Length + @Matches (复杂度要求) |
邮箱 | @IsEmail + @MaxLength(254) |
手机号 | 自定义校验装饰器(如@IsPhone() ) |
💡 最佳实践:将常用正则表达式提取为常量复用
3.2 控制器应用DTO
// auth.controller.ts
import { Body, Controller, Post, UnprocessableEntityException } from '@nestjs/common';
import { SignInUserDto } from './dto/sign-in-user.dto';
@Controller('auth')
export class AuthController {
@Post('login')
async login(@Body() signInDto: SignInUserDto) {
// 自动校验通过后执行
return {
code: 200,
data: {
username: signInDto.username,
lastLogin: new Date()
}
};
}
@Post('register')
async register(@Body() signInDto: SignInUserDto) {
// 手动触发校验示例
const errors = await validate(signInDto);
if (errors.length > 0) {
throw new UnprocessableEntityException({
code: 422,
message: '参数校验失败',
details: errors
});
}
// 业务逻辑...
}
}
typescript
控制器最佳实践
- 自动校验
- 依赖
ValidationPipe
自动处理 - 错误时返回标准格式:
{ "statusCode": 400, "message": ["username must be longer than or equal to 6 characters"], "error": "Bad Request" }
json
- 依赖
- 手动校验场景
- 动态DTO校验
- 条件校验(使用
validate()
方法)
- 响应标准化
// 统一响应格式 throw new BadRequestException({ code: 40001, message: '参数校验失败', details: errors });
typescript
进阶用法:动态DTO
// 根据用户类型动态校验
class DynamicDTO {
@IsIn(['admin', 'user'])
type: string;
@ValidateIf(o => o.type === 'admin')
@IsString()
adminToken?: string;
}
typescript
扩展学习
- 校验性能优化
// 禁用详细错误提升性能 new ValidationPipe({ disableErrorMessages: true })
typescript - 自定义校验装饰器
// 实现唯一性校验 @ValidatorConstraint({ async: true }) export class IsUniqueConstraint implements ValidatorConstraintInterface { async validate(value: any, args: ValidationArguments) { const repo = getRepository(User); return !(await repo.existsBy({ [args.property]: value })); } }
typescript - Swagger集成
@ApiProperty({ description: '用户名', minLength: 6, maxLength: 20, example: 'user123' }) username: string;
typescript
💡 生产建议:
- 对敏感字段(如密码)添加
@Exclude()
防止日志泄露 - 使用
@Transform()
对输入数据进行预处理 - 通过
groups
实现创建/更新不同的校验规则
四、校验效果与自定义
4.1 常见校验场景
典型校验场景分析
- 基础校验失败
// 请求体 { "username": 123 } // 响应 { "statusCode": 400, "message": ["username must be a string"] }
json - 复合规则触发
@IsEmail() @MaxLength(254) email: string; // 触发条件: // - 非邮箱格式 // - 长度超过254字符
typescript - 嵌套对象校验
class AddressDto { @IsPostalCode('CN') postalCode: string; } @ValidateNested() address: AddressDto;
typescript
💡 调试技巧:
使用ValidationPipe
的exceptionFactory
捕获原始错误对象:
new ValidationPipe({
exceptionFactory: (errors) => {
console.log('原始校验错误:', errors);
return new BadRequestException(errors);
}
})
typescript
4.2 自定义错误消息
动态消息生成进阶
@IsIn(['admin', 'user'], {
message: (args) => {
const validValues = args.constraints[0];
return `角色必须是以下值之一: ${validValues.join(', ')}. 当前值: ${args.value}`;
}
})
role: string;
typescript
多语言支持方案
// 创建消息解析器
const i18nMessages = {
en: {
'username.required': 'Username is required',
'username.length': 'Username must be 6-20 characters'
},
zh: {
'username.required': '用户名不能为空',
'username.length': '用户名需6-20位字符'
}
};
// 在DTO中使用
@IsNotEmpty({ message: 'username.required' })
@Length(6, 20, { message: 'username.length' })
username: string;
// 中间件处理翻译
app.use((req, res, next) => {
const lang = req.headers['accept-language'] || 'en';
ValidatorConfig.message = (key) => i18nMessages[lang][key];
next();
});
typescript
错误消息格式化
// 统一错误结构
class ValidationError {
constructor(
public field: string,
public code: string,
public message: string,
public rejectedValue: any
) {}
}
// 转换原始错误
const formattedErrors = errors.map(error =>
new ValidationError(
error.property,
error.constraints[Object.keys(error.constraints)[0]],
error.constraints[Object.keys(error.constraints)[0]],
error.value
)
);
typescript
4.3 自定义校验装饰器
完整校验器实现案例
import {
ValidatorConstraint,
ValidatorConstraintInterface,
registerDecorator
} from 'class-validator';
@ValidatorConstraint({ name: 'isFutureDate', async: false })
export class IsFutureDateValidator implements ValidatorConstraintInterface {
validate(date: Date) {
return date.getTime() > Date.now();
}
defaultMessage() {
return '日期必须是将来的时间';
}
}
// 装饰器工厂
export function IsFutureDate(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsFutureDateValidator
});
};
}
// 使用示例
export class EventDto {
@IsFutureDate()
startTime: Date;
}
typescript
高级校验场景
- 跨字段校验
@ValidatorConstraint({ name: 'isDateRangeValid' }) export class DateRangeValidator implements ValidatorConstraintInterface { validate(value: any, args: ValidationArguments) { const [relatedPropertyName] = args.constraints; const relatedValue = args.object[relatedPropertyName]; return value > relatedValue; } }
typescript - 异步数据库校验
@ValidatorConstraint({ name: 'isUserExist', async: true }) export class IsUserExistValidator implements ValidatorConstraintInterface { constructor(private userService: UserService) {} async validate(email: string) { return !(await this.userService.existsByEmail(email)); } }
typescript
校验器注册方式
// 全局注册(推荐)
@Module({
providers: [IsFutureDateValidator, IsUserExistValidator]
})
export class AppModule {}
// 局部注册
@Injectable()
export class UserService {
constructor(
@InjectValidator()
private validator: IsUserExistValidator
) {}
}
typescript
扩展工具推荐
- 可视化调试工具
class-validator可视化调试器 - 性能分析
使用class-validator
的validator.validate()
方法进行基准测试:console.time('validation'); await validate(dto); console.timeEnd('validation');
typescript - 企业级方案
- 阿里云OpenAPI校验规范
- JSON Schema校验器ajv的集成
💡 架构建议:
对于高频接口,可将校验规则编译为JSON Schema,使用WebAssembly加速校验。
↑