大家好,我是汪小成。 🚗💨
在上一篇中,我们成功运用NestJS的核心三剑客(模块、控制器、服务)和依赖注入,从零搭建了一个功能完备(虽然是内存版)的用户管理CRUD API。是不是感觉NestJS的开发思路清晰又高效?
但是,冷静下来想一想,我们当前的API是不是有点"太天真"了?
- 用户创建一个新用户时,如果
username
不小心传了个空字符串怎么办?😱 - 更新用户状态时,如果客户端传来的
status
根本不是我们定义的状态值,又该如何处理?🤯 - 现在我们在Controller里直接用
@Body('username')
这种方式一个个取参数,如果参数多了,代码会不会变得很长很乱?📝
这些问题如果不解决,我们的API就像一个没穿盔甲的士兵,面对各种"非法入侵"(无效数据),脆弱不堪!
别担心,NestJS早就为我们准备好了坚固的铠甲!今天,我们将学习两个强大的武器:
- DTO (Data Transfer Object - 数据传输对象): 规范进出API的数据结构。
- ValidationPipe : 结合
class-validator
库,实现对输入数据的自动化验证。
准备好让API变得刀枪不入了吗?Let's gear up!
🧐 什么是 DTO?为何需要它?
DTO (Data Transfer Object) ,顾名思义,就是专门用来在不同层(比如网络请求到Controller,或者Service到Controller)之间传输数据的对象。
在NestJS中,我们通常使用**TypeScript类(Class)**来定义DTO。
为什么不用Interface呢? 因为Class在编译后是真实存在的JavaScript构造函数,它可以附加装饰器 (Decorators) ,这对于我们后面要做的数据验证至关重要!Interface 只是编译时的类型检查,无法附加装饰器。
使用DTO的好处:
- 明确数据结构: 清晰地定义了API端点期望接收或返回的数据格式。
- 类型安全: 配合TypeScript,提供编译时和运行时的类型检查。
- 代码整洁: 将数据结构定义与业务逻辑分离。
- 验证载体: 作为应用验证规则的理想场所。
通过下面这个数据表,更能直观地了解DTO Class与Interface的不同:
特性 | DTO (Class) | Interface |
---|---|---|
运行时存在 | ✅ 是 | ❌ 否 |
可加装饰器 | ✅ 是 | ❌ 否 |
类型检查 | ✅ 编译时+运行时 | ✅ 仅编译时 |
✍️ 实战:为 User API 定义 DTO
让我们为之前的UserController
中的创建用户和更新状态操作定义DTO。
- 安装必要的库
bash
pnpm install class-validator class-transformer
说明: (class-transformer
通常和class-validator
一起使用,它可以帮助自动转换传入数据类型,ValidationPipe
会用到它)。
- 在
src/user
目录下,创建dto
文件夹,并在其中创建两个文件:
create-user.dto.ts
: 用于创建用户时校验请求体。
typescript
// src/user/dto/create-user.dto.ts
import { IsNotEmpty, IsString } from 'class-validator'; // 引入验证装饰器
export class CreateUserDto {
@IsNotEmpty() // 验证规则:不能为空
@IsString() // 验证规则:必须是字符串
username: string;
@IsNotEmpty()
@IsString()
password: string;
}
update-user-status.dto.ts
: 用于更新用户状态时校验请求体。
typescript
// src/user/dto/update-user-status.dto.ts
import { IsEnum } from 'class-validator';
import { UserStatus } from '../user.model'; // 引入我们定义的枚举
export class UpdateUserStatusDto {
@IsEnum(UserStatus) // 验证规则:值必须是UserStatus枚举中的一个
status: UserStatus;
}
看到了吗?我们直接在DTO类的属性上使用了@IsNotEmpty()
, @IsString()
, @IsEnum()
这些装饰器 。这些装饰器来自于class-validator
库,它们就是我们给API输入定义的验证规则。
🛡️ 引入ValidationPipe:自动化验证的魔法
定义好了DTO和验证规则,谁来执行验证呢?这就是 Pipe (管道) 的工作了。NestJS提供了一个非常方便的内置Pipe ------ ValidationPipe
。
ValidationPipe
会自动读取你在DTO上设置的class-validator
装饰器,并对进入Controller方法的请求体 (或其他参数) 进行校验。如果校验失败,它会自动抛出一个包含详细错误信息的BadRequestException
(400 错误),阻止无效请求进入业务逻辑!
如何使用它?最简单的方式是全局启用 。修改 src/main.ts
文件:
typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; // 导入 ValidationPipe
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局启用ValidationPipe
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
加上app.useGlobalPipes(new ValidationPipe());
这一行,魔法就生效了!现在,所有进入Controller的、使用了DTO类型注解的请求体,都会自动经过验证。
⚙️ 重构Controller:拥抱DTO
现在,我们可以用定义好的DTO来简化并加固UserController
了:
typescript
// src/user/user.controller.ts
import {
Controller,
Get,
Post,
Body,
Param,
Delete,
Patch,
} from '@nestjs/common';
import { UserService } from './user.service';
import { User, UserStatus } from './user.model';
// 引入我们创建的 DTO
import { CreateUserDto } from './dto/create-user.dto.ts';
import { UpdateUserStatusDto } from './dto/update-user-status.dto.ts';
@Controller('user')
export class UserController {
constructor(private usersService: UserService) {}
// ... 其它未变化的代码 ...
// 创建用户
@Post()
// 看这里!@Body() 的类型直接指定为CreateUserDto
// 不再需要@Body('username')和@Body('password')了
createUser(@Body() createUserDto: CreateUserDto): User {
// ValidationPipe会在调用这个方法前自动验证createUserDto
// 如果验证通过,createUserDto就是一个包含username和password的对象
return this.usersService.createUser(createUserDto.username, createUserDto.password);
}
// 更新用户状态 (使用 UpdateUserStatusDto)
@Patch('/:id/status')
// 看这里!@Body()的类型指定为UpdateUserStatusDto
updateUserStatus(
@Param('id') id: string,
@Body() updateUserStatusDto: UpdateUserStatusDto, // 直接获取 DTO
): User {
// ValidationPipe会自动验证status字段是否为有效的UserStatus枚举值
return this.usersService.updateUserStatus(id, updateUserStatusDto.status);
}
}
看到了吗?Controller的代码变得更简洁、更具表达力了!
- 我们不再需要手动从
@Body()
中提取每个字段。 - 参数类型直接使用我们定义的DTO类。
- 验证逻辑完全委托给了
ValidationPipe
,控制器只需关注调用Service。
✅ 测试验证效果:让非法数据无所遁形!
重启应用 (pnpm run start:dev
),打开Postman:
1. 测试创建用户
-
发送无效数据: Body设置为
{"password": "123456"}
(缺少username
)。 -
预期结果: 你会收到一个 400 Bad Request 响应,响应体类似:
json{ "message": [ "username must be a string", "username should not be empty" ], "error": "Bad Request", "statusCode": 400 }
ValidationPipe
自动返回了详细的错误信息!
2.测试更新状态
-
发送无效数据: Body设置为
{"status": "TEST"}
。 -
预期结果: 收到400 Bad Request,消息类似:
json{ "message": [ "status must be one of the following values: NORMAL, LOCKED" ], "error": "Bad Request", "statusCode": 400 }
💡 总结:DTO 与 ValidationPipe 的价值
今天,我们给API穿上了坚固的"铠甲":
- 使用DTO(类) 规范了数据传输结构。
- 利用
class-validator
装饰器 定义了清晰的验证规则。 - 通过全局
ValidationPipe
实现了输入的自动化验证。
这带来的好处显而易见:
- 增强了API的健壮性和安全性,有效防止无效数据污染业务逻辑。
- 提升了开发体验 (DX),验证逻辑与业务逻辑解耦,Controller代码更简洁。
- API契约更明确,前后端协作更顺畅。
附录
class-validator常用装饰器
分类 | 装饰器 | 作用 | 示例 |
---|---|---|---|
常见验证 | @IsString() |
验证是否为字符串 | @IsString() |
@IsNumber() |
验证是否为数字(可配置选项) | @IsNumber({ maxDecimalPlaces: 2 }) |
|
@IsBoolean() |
验证是否为布尔值 | @IsBoolean() |
|
@IsDate() |
验证是否为日期对象 | @IsDate() |
|
空值检查 | @IsOptional() |
允许字段为null 或undefined (跳过其他验证) |
@IsOptional() |
@IsDefined() |
验证字段必须存在(非null 且非undefined ) |
@IsDefined() |
|
范围限制 | @Min() |
数字最小值限制 | @Min(0) |
@Max() |
数字最大值限制 | @Max(100) |
|
@Length(min, max) |
字符串/数组长度范围 | @Length(2, 20) |
|
格式验证 | @IsEmail() |
验证是否为合法邮箱格式(可配置宽松模式) | @IsEmail({ allow_display_name: true }) |
@IsUrl() |
验证是否为合法URL(可配置协议、域名等) | @IsUrl({ protocols: ['https'] }) |
|
@Matches(regex) |
正则表达式匹配 | @Matches(/^[A-Za-z]+$/) |
|
逻辑组合 | @ValidateIf(条件函数) |
根据条件决定是否验证其他装饰器 | @ValidateIf(o => o.age > 18) |
@ValidateNested() |
验证嵌套对象(需配合@Type 使用) |
@ValidateNested() |
|
类型检查 | @IsArray() |
验证是否为数组 | @IsArray() |
@IsEnum() |
验证是否为指定枚举值 | @IsEnum(UserRole) |
|
特殊值 | @IsEmpty() |
验证字段必须为空值(null 或undefined ) |
@IsEmpty() |
@IsNotEmpty() |
验证字段非空(字符串/数组长度 > 0) | @IsNotEmpty() |
|
自定义 | @CustomValidator() |
自定义验证逻辑(需实现ValidatorConstraintInterface ) |
需配合自定义类使用 |
📢 互动与支持
保持关注,不要走开!
如果你觉得这篇文章对你有帮助,请不吝:点赞 👍 、在看 👀 、转发 ↗️ ,让更多朋友加入我们的NestJS探索之旅!
💬 欢迎在评论区留言分享你的想法或疑问,我们一起交流进步!
🔔 关注我的公众号「Java小成」 ,第一时间获取 《NestJS学习笔记》系列文章更新!
本文代码已同步至GitHub:
https://github.com/wanggch/learn-nestjs/tree/main/03.user-validator
。