Skip to content

当然!我们来探讨一个能极大提升你应用健壮性和安全性的核心技术:验证 (Validation)。这就像是为你的应用聘请了一位火眼金睛、一丝不苟的“门卫”,确保任何试图进入你系统的“访客”(数据)都必须符合规矩。

为你的应用聘请一位“火眼金睛”的门卫:NestJS 验证终极指南

想象你的应用是一家高档餐厅的厨房。

  • 服务员(前端或客户端) 会将客人的**点菜单(传入的数据,如 JSON Body)**递送到厨房门口。
  • 你(Controller 的处理函数) 是主厨,准备根据点菜单大展身手。

但如果服务员递过来一张潦草的、不合规矩的点菜单怎么办?

  • “我要一份‘一百二十’块牛排。” (price 应该是 number,却传了 string
  • “随便来点吃的。” (没有提供必需的菜品名称 name
  • “我要一份名字超级无敌长长长到写不下的惠灵顿牛排……” (name 超过了数据库字段长度限制)

如果你作为主厨,不加检查就直接开始做菜,厨房肯定会乱成一锅粥,甚至可能引发事故(系统崩溃、存入脏数据)。

你需要一个专业的迎宾/门卫(Validation Pipe) 站在厨房门口。他会拿到每一份点菜单,用一本**《餐厅点餐规范手册》(DTO 和验证规则)** 来仔细核对:

  1. 必填项(菜名)写了吗?
  2. 价格是数字吗?
  3. 菜名长度在规定范围内吗?

只有完全合格的点菜单,才能被递到主厨手里。不合格的,门卫会直接退回给服务员并告知原因(返回一个清晰的 400 Bad Request 错误)。

在 NestJS 中,这个“门卫”就是 ValidationPipe,而那本“规范手册”就是我们用 class-validatorclass-transformer 这两个库来定义的 DTO (Data Transfer Object)

1. 第一步:准备“规范手册” (DTO)

前置知识:什么是 DTO (数据传输对象)?

DTO 是一个专门用来在不同进程或应用层之间传输数据的对象。在 NestJS 中,我们通常使用类 (Class) 来定义 DTO。

为什么用 class 而不是 interface 因为 TypeScript 的 interface 在代码被编译成 JavaScript 后就消失了,我们无法在运行时获取它的信息。而 class 会被保留下来,这使得我们可以给它的属性附加装饰器 (Decorators),而这些装饰器正是我们定义验证规则的关键。

让我们来创建一个“创建猫猫”的点菜单 CreateCatDto

src/cats/dto/create-cat.dto.ts

typescript
export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}
```这只是一个骨架,接下来我们要为它添加规则。

### 2. 第二步:为“手册”编写规则 (使用 `class-validator`)

我们需要两个强大的“魔法工具”来编写规则:

*   **`class-validator`**: 提供了海量的验证装饰器(如 `@IsString`, `@IsInt`, `@MinLength`),让你用声明式的方式来定义一个属性应该满足什么条件。
*   **`class-transformer`**: 它的主要作用是将普通的 JavaScript 对象转换为我们定义的 DTO 类的实例,并可以自动进行一些类型转换。`ValidationPipe` 在后台会自动使用它。

**首先,安装它们:**
```bash
npm install class-validator class-transformer

现在,让我们用验证装饰器来丰富我们的 DTO:

src/cats/dto/create-cat.dto.ts (添加规则后)

typescript
import { IsString, IsInt, Min, Max, MinLength } from 'class-validator';

export class CreateCatDto {
  @IsString({ message: '猫猫的名字必须是字符串' }) // 验证 name 是不是字符串
  @MinLength(2, { message: '名字太短了,至少需要2个字符' }) // 最小长度
  name: string;

  @IsInt() // 验证 age 是不是整数
  @Min(0) // 最小值为 0
  @Max(25) // 最大值为 25
  age: number;

  @IsString()
  breed: string;
}

现在我们的“规范手册”已经非常详细和严格了。

3. 第三步:聘请“门卫” (ValidationPipe)

有了手册,我们还需要一个执行者。NestJS 提供了一个内置的 ValidationPipe,它能自动读取 DTO 上的验证装饰器并执行校验。

最简单、最推荐的聘请方式,就是让他成为一个全局的门卫,检查所有进入应用的请求。我们在 main.ts 中进行配置。

src/main.ts

typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 聘请一位全局的门卫
  app.useGlobalPipes(new ValidationPipe());

  await app.listen(3000);
}
bootstrap();

就这样,一行代码,你的应用现在就有了一位全天候、高度负责的门卫了!

4. 第四步:观察“门卫”的工作流程

现在,让我们看看当点菜单(请求)递过来时,会发生什么。

src/cats/cats.controller.ts

typescript
import { Controller, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';

@Controller('cats')
export class CatsController {
  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    // 如果代码能执行到这里,说明 createCatDto 100% 是通过了验证的
    // 而且它已经是一个 CreateCatDto 的实例,而不是一个普通 JS 对象
    console.log('点菜单合格,主厨开始做菜!', createCatDto);
    return `This action adds a new cat named ${createCatDto.name}`;
  }
}

场景 A:一份合格的点菜单

客户端发送 POST 请求到 /cats,请求体为:

json
{
  "name": "Garfield",
  "age": 7,
  "breed": "Exotic Shorthair"
}

流程:

  1. 请求到达 NestJS。
  2. ValidationPipe (全局门卫) 拦截请求体。
  3. 门卫拿出 CreateCatDto (规范手册),逐一核对:name 是字符串且长度>2?是。age 是整数且在 0-25 之间?是。breed 是字符串?是。
  4. 全部通过! 门卫将这份合格的点菜单递给了主厨(create 方法)。
  5. 控制台打印 "点菜单合格...",客户端收到 201 成功响应。

场景 B:一份不合格的点菜单

客户端发送 POST 请求,请求体为:

json
{
  "name": "G",
  "age": "abc",
  "breed": "Exotic Shorthair"
}

流程:

  1. 请求到达 NestJS。
  2. ValidationPipe 开始核对。
  3. 发现问题!
    • name: "G" 的长度小于 2,违反 @MinLength(2)
    • age: "abc" 不是整数,违反 @IsInt()
  4. 不合格! 门卫直接拒绝这份点菜单,根本不会把它递给主厨
  5. ValidationPipe 自动抛出一个 BadRequestException 异常。
  6. NestJS 捕获这个异常,并给客户端返回一个格式清晰的 400 Bad Request 错误响应:
json
{
  "statusCode": 400,
  "message": ["名字太短了,至少需要2个字符", "age must be an integer number"],
  "error": "Bad Request"
}

看,这个自动生成的错误信息多么友好和清晰!它直接告诉了客户端哪里填错了。

5. “门卫”的额外技能:数据转换

ValidationPipe 还有一个隐藏的超能力:自动类型转换

比如,一个 GET 请求的查询参数总是字符串类型:GET /cats?age=5。这里的 5 是一个字符串 '5'

如果我们开启转换功能,ValidationPipe 可以自动把它变成数字 5

main.ts (开启转换)

typescript
app.useGlobalPipes(
  new ValidationPipe({
    transform: true, // 开启自动转换
  })
);

现在,在 DTO 中,我们可以用 class-transformer@Type 装饰器来辅助转换。

typescript
// find-cats.dto.ts
import { Type } from 'class-transformer';
import { IsInt, IsOptional } from 'class-validator';

export class FindCatsDto {
  @IsInt()
  @IsOptional() // 标记为可选参数
  @Type(() => Number) // 告诉 ValidationPipe,请尝试将这个值转换为 Number
  age?: number;
}

// cats.controller.ts
@Get()
findAll(@Query() query: FindCatsDto) {
  // 因为开启了 transform,这里的 query.age 就已经是 number 类型了!
  console.log(typeof query.age); // "number"
  // ...
}

总结

验证是构建可靠 API 的第一道防线。NestJS 提供的 ValidationPipe 配合 class-validatorclass-transformer,形成了一套自动化、声明式、功能强大的验证工作流。

  1. 定义 DTO (class):用类作为数据传输的“规范手册”。
  2. 添加规则 (decorators):用 @IsString() 等装饰器在 DTO 上声明验证规则。
  3. 全局启用 (ValidationPipe):在 main.ts 中全局启用 ValidationPipe,让“门卫”自动上岗。

遵循这个模式,你的 Controller 代码会变得极其整洁,因为它不再需要充斥着 if (typeof name !== 'string') 这样的手动校验逻辑。所有的验证工作都交给了这位可靠的“门卫”,让你可以安心地在“厨房”里专注于核心的业务逻辑。