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

相关推荐
qq_12498707534 分钟前
基于springboot的智能医院挂号系统(源码+论文+部署+安装)
java·人工智能·spring boot·后端·毕业设计
木木一直在哭泣9 分钟前
ThreadLocal 讲清楚:它是什么、为什么会“内存泄漏”、线程池复用为什么会串号
后端
艺杯羹13 分钟前
Thymeleaf模板引擎:让Spring Boot页面开发更简单高效
java·spring boot·后端·thymeleadf
逸风尊者33 分钟前
开发可掌握的知识:推荐系统
java·后端·算法
Violet_YSWY37 分钟前
阿里巴巴状态码
后端
灵魂猎手41 分钟前
Antrl4 入门 —— 使用Antrl4实现一个表达式计算器
java·后端
moxiaoran57531 小时前
Go语言的递归函数
开发语言·后端·golang
IT 行者1 小时前
Spring Security 7.0 新特性详解
java·后端·spring
华仔啊1 小时前
Java 的金额计算用 long 还是 BigDecimal?资深程序员这样选
java·后端