NestJS学习笔记-03-使用class-validator进行接口参数校验🚀

大家好,我是汪小成。 🚗💨

在上一篇中,我们成功运用NestJS的核心三剑客(模块、控制器、服务)和依赖注入,从零搭建了一个功能完备(虽然是内存版)的用户管理CRUD API。是不是感觉NestJS的开发思路清晰又高效?

但是,冷静下来想一想,我们当前的API是不是有点"太天真"了?

  • 用户创建一个新用户时,如果username不小心传了个空字符串怎么办?😱
  • 更新用户状态时,如果客户端传来的status根本不是我们定义的状态值,又该如何处理?🤯
  • 现在我们在Controller里直接用@Body('username')这种方式一个个取参数,如果参数多了,代码会不会变得很长很乱?📝

这些问题如果不解决,我们的API就像一个没穿盔甲的士兵,面对各种"非法入侵"(无效数据),脆弱不堪

别担心,NestJS早就为我们准备好了坚固的铠甲!今天,我们将学习两个强大的武器:

  1. DTO (Data Transfer Object - 数据传输对象): 规范进出API的数据结构。
  2. 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。

  1. 安装必要的库
bash 复制代码
pnpm install class-validator class-transformer

说明: (class-transformer通常和class-validator一起使用,它可以帮助自动转换传入数据类型,ValidationPipe会用到它)。

  1. 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() 允许字段为nullundefined(跳过其他验证) @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() 验证字段必须为空值(nullundefined @IsEmpty()
@IsNotEmpty() 验证字段非空(字符串/数组长度 > 0) @IsNotEmpty()
自定义 @CustomValidator() 自定义验证逻辑(需实现ValidatorConstraintInterface 需配合自定义类使用

📢 互动与支持

保持关注,不要走开!

如果你觉得这篇文章对你有帮助,请不吝:点赞 👍在看 👀转发 ↗️ ,让更多朋友加入我们的NestJS探索之旅

💬 欢迎在评论区留言分享你的想法或疑问,我们一起交流进步!

🔔 关注我的公众号「Java小成」 ,第一时间获取 《NestJS学习笔记》系列文章更新

本文代码已同步至GitHub:https://github.com/wanggch/learn-nestjs/tree/main/03.user-validator

相关推荐
海天胜景29 分钟前
HTTP Error 500.31 - Failed to load ASP.NET Core runtime
后端·asp.net
海天胜景32 分钟前
Asp.Net Core IIS发布后PUT、DELETE请求错误405
数据库·后端·asp.net
源码云商2 小时前
Spring Boot + Vue 实现在线视频教育平台
vue.js·spring boot·后端
RunsenLIu4 小时前
基于Django实现的篮球论坛管理系统
后端·python·django
HelloZheQ6 小时前
Go:简洁高效,构建现代应用的利器
开发语言·后端·golang
caihuayuan56 小时前
[数据库之十四] 数据库索引之位图索引
java·大数据·spring boot·后端·课程设计
风象南7 小时前
Redis中6种缓存更新策略
redis·后端
程序员Bears7 小时前
Django进阶:用户认证、REST API与Celery异步任务全解析
后端·python·django
非晓为骁7 小时前
【Go】优化文件下载处理:从多级复制到零拷贝流式处理
开发语言·后端·性能优化·golang·零拷贝
北极象8 小时前
Golang中集合相关的库
开发语言·后端·golang