第 185 章:数据验证与 DTO
学习目标
- 深入理解 DTO 模式
- 掌握 class-validator 全部装饰器
- 学会自定义验证规则
- 理解嵌套与分组验证
一、什么是 DTO
DTO (Data Transfer Object) 用于在层之间传输数据,通常带验证规则。
二、基础 DTO
2.1 Create DTO
typescript
import { IsString, IsEmail, MinLength, MaxLength, IsInt, Min, Max, IsOptional, IsEnum } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2, { message: '姓名至少 2 位' })
@MaxLength(20)
name: string;
@IsEmail({}, { message: '邮箱格式不正确' })
email: string;
@IsString()
@MinLength(6)
password: string;
@IsOptional()
@IsInt()
@Min(0)
@Max(150)
age?: number;
@IsEnum(['admin', 'user'], { message: '角色必须是 admin 或 user' })
role: 'admin' | 'user';
}2.2 Update DTO(部分字段)
typescript
import { PartialType } from '@nestjs/mapped-types';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
// 等价于
export class UpdateUserDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
@IsEmail()
email?: string;
// ... 所有字段都变可选
}2.3 Query DTO(查询参数)
typescript
export class PaginationDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
size: number = 10;
@IsOptional()
@IsString()
@MaxLength(50)
keyword?: string;
@IsOptional()
@IsIn(['ASC', 'DESC'])
order?: 'ASC' | 'DESC' = 'DESC';
}三、PickType / OmitType
3.1 Pick(挑字段)
typescript
import { PickType } from '@nestjs/mapped-types';
export class LoginDto extends PickType(CreateUserDto, ['email', 'password']) {}
// 只有 email 和 password3.2 Omit(排除字段)
typescript
import { OmitType } from '@nestjs/mapped-types';
export class SafeUserDto extends OmitType(CreateUserDto, ['password']) {}
// 排除 password四、嵌套验证
4.1 嵌套对象
typescript
export class AddressDto {
@IsString()
city: string;
@IsString()
street: string;
@IsString()
zipCode: string;
}
export class CreateUserDto {
@IsString()
name: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}4.2 嵌套数组
typescript
export class CreateOrderDto {
@IsString()
customerId: string;
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items: OrderItemDto[];
}
export class OrderItemDto {
@IsString()
productId: string;
@IsInt()
@Min(1)
quantity: number;
}五、自定义验证装饰器
5.1 简单规则
typescript
import { registerDecorator, ValidationOptions } from 'class-validator';
export function IsStrongPassword(options?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isStrongPassword',
target: object.constructor,
propertyName,
options,
validator: {
validate(value: any) {
return typeof value === 'string'
&& /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(value);
},
defaultMessage() {
return '密码至少 8 位,包含大小写字母和数字';
},
},
});
};
}5.2 关联字段验证
typescript
// 确认密码必须相同
export function IsMatch(property: string, options?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isMatch',
target: object.constructor,
propertyName,
constraints: [property],
options,
validator: {
validate(value: any, args: any) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return value === relatedValue;
},
defaultMessage() {
return '两次输入不一致';
},
},
});
};
}
export class RegisterDto {
@IsStrongPassword()
password: string;
@IsMatch('password')
confirmPassword: string;
}5.3 异步验证
typescript
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';
import { Injectable } from '@nestjs/common';
import { UserService } from './user.service';
@ValidatorConstraint({ name: 'isEmailUnique', async: true })
@Injectable()
export class IsEmailUniqueConstraint implements ValidatorConstraintInterface {
constructor(private userService: UserService) {}
async validate(email: string) {
const user = await this.userService.findByEmail(email);
return !user;
}
defaultMessage() {
return '邮箱已被注册';
}
}
export function IsEmailUnique(options?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName,
options,
validator: IsEmailUniqueConstraint,
});
};
}
// 使用
export class CreateUserDto {
@IsEmailUnique()
email: string;
}
// module
@Module({
providers: [IsEmailUniqueConstraint], // 注册
})
export class UserModule {}六、分组验证
6.1 定义分组
typescript
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
@Validate(IsEmailUnique)
email: string;
@IsString()
@MinLength(6)
password: string;
}6.2 不同接口用不同校验
typescript
@Post()
create(
@Body(new ValidationPipe({ groups: ['create'] })) dto: CreateUserDto,
) {}
@Patch(':id')
update(
@Param('id') id: string,
@Body(new ValidationPipe({ groups: ['update'] })) dto: UpdateUserDto,
) {}七、class-transformer
7.1 类型转换
typescript
import { Type, Transform } from 'class-transformer';
export class SearchDto {
@Type(() => Number) // 字符串转数字
page: number;
@Type(() => Date) // 字符串转 Date
startDate: Date;
}7.2 字段转换
typescript
import { Transform } from 'class-transformer';
// 自动 trim
@Transform(({ value }) => value?.trim())
name: string;
// 转小写
@Transform(({ value }) => value?.toLowerCase())
email: string;
// 数字加千分位
@Transform(({ value }) => value?.toLocaleString())
amount: number;八、错误消息定制
8.1 全局定制
typescript
app.useGlobalPipes(new ValidationPipe({
exceptionFactory: (errors) => {
const messages = errors
.map(err => Object.values(err.constraints || {}).join(', '))
.join('; ');
return new BadRequestException({
code: 400,
message: messages,
data: null,
});
},
}));8.2 详细错误
typescript
app.useGlobalPipes(new ValidationPipe({
exceptionFactory: (errors) => {
const formatted = errors.map(err => ({
field: err.property,
errors: Object.values(err.constraints || {}),
}));
return new BadRequestException({
code: 400,
message: 'Validation failed',
errors: formatted,
});
},
}));输出:
json
{
"code": 400,
"message": "Validation failed",
"errors": [
{ "field": "email", "errors": ["邮箱格式不正确"] },
{ "field": "password", "errors": ["密码至少 8 位"] }
]
}九、嵌套 + 条件验证
typescript
export class CreateEventDto {
@IsString()
name: string;
@IsEnum(['online', 'offline'])
type: 'online' | 'offline';
// 在线 → 不需要 location
// 线下 → 需要 location
@ValidateIf(o => o.type === 'offline')
@IsString()
location?: string;
// 在线 → 需要 url
@ValidateIf(o => o.type === 'online')
@IsUrl()
url?: string;
}十、常用验证装饰器速查
| 类别 | 装饰器 |
|---|---|
| 类型 | @IsString @IsInt @IsBoolean @IsDate @IsNumber |
| 字符串 | @MinLength @MaxLength @Length @IsEmail @IsUrl @IsUUID @IsAlpha @IsAlphanumeric |
| 数字 | @Min @Max @IsPositive @IsNegative |
| 数组 | @ArrayMinSize @ArrayMaxSize @ArrayContains @ArrayUnique |
| 对象 | @ValidateNested @IsInstance @IsDefined |
| 日期 | @MinDate @MaxDate |
| 通用 | @IsOptional @IsNotEmpty @IsEmpty @Equals @NotEquals |
| 枚举 | @IsEnum @IsIn |
| 格式 | @IsJSON @IsBase64 @IsHexColor |
| 网络 | @IsIP @IsPort @IsMimeType |
十一、本章小结
| 概念 | 关键 |
|---|---|
| DTO | 数据传输对象 |
| ValidationPipe | 全局验证 |
| PartialType | 部分字段 |
| PickType / OmitType | 选/排除字段 |
| @ValidateNested | 嵌套 |
| @ValidateIf | 条件验证 |
| registerDecorator | 自定义规则 |
动手练习
- 创建 RegisterDto,带密码确认
- 创建一个嵌套 DTO(用户 + 地址)
- 写一个
@IsPhone自定义验证器 - 用
ValidateIf实现条件必填