Nest ValidationPipe 参数验证 - 就像前端的表单校验

0 阅读4分钟

1. 先说前端的表单验证

你有没有被表单验证折磨过?

// 前端表单验证
const form = {
  username: '',
  age: 18,
  email: ''
};

// 手动验证
if (!form.username) {
  alert('用户名不能为空');
}
if (form.age < 0 || form.age > 150) {
  alert('年龄不合法');
}
if (!form.email.includes('@')) {
  alert('邮箱格式不对');
}

太麻烦了!有没有更优雅的方式?

后来 Element Plus 推出了表单验证功能:

<!-- Element Plus 表单验证 -->
<el-form :model="form" :rules="rules" ref="formRef">
  <el-form-item prop="username">
    <el-input v-model="form.username" />
  </el-form-item>
  <el-form-item prop="age">
    <el-input v-model.number="form.age" />
  </el-form-item>
  <el-form-item prop="email">
    <el-input v-model="form.email" />
  </el-form-item>
</el-form>

<script setup>
const form = reactive({
  username: '',
  age: 18,
  email: ''
});

const rules = {
  username: [
    { required: true, message: '用户名不能为空', trigger: 'blur' },
    { min: 3, message: '用户名至少3个字符', trigger: 'blur' }
  ],
  age: [
    { required: true, message: '年龄不能为空', trigger: 'blur' },
    { type: 'number', min: 0, max: 150, message: '年龄范围 0-150', trigger: 'blur' }
  ],
  email: [
    { required: true, message: '邮箱不能为空', trigger: 'blur' },
    { type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
  ]
};
</script>

声明式验证,写一次,到处都用。


2. 后端也需要验证!

前端验证只是第一道防线,后端验证才是最后一道防线

如果有人绕过前端直接调接口咋办?

// Post 请求接收数据
@Post('/user')
createUser(@Body() body: any) {
  // 万一 age 传个 "hello" 字符串咋办?
  console.log(body.age + 1); // "hello" + 1 = "hello1" ???
}

后端必须验证!


3. Nest 怎么办?用 ValidationPipe!

第 1 步:安装依赖

npm install class-validator class-transformer

第 2 步:创建 DTO

// user.dto.ts
export class CreateUserDto {
  @IsString()
  @MinLength(3)
  username: string;

  @IsInt()
  @Min(0)
  @Max(150)
  age: number;

  @IsEmail()
  email: string;
}

DTO = Data Transfer Object,就是"数据传输对象",用来接收请求体的数据。

对比前端 Element Plus(rules 配置):

const rules = {
  username: [
    { required: true, message: '用户名不能为空', trigger: 'blur' },
    { min: 3, message: '用户名至少3个字符', trigger: 'blur' }
  ],
  age: [
    { required: true, message: '年龄不能为空', trigger: 'blur' },
    { type: 'number', min: 0, max: 150, message: '年龄范围 0-150', trigger: 'blur' }
  ],
  email: [
    { required: true, message: '邮箱不能为空', trigger: 'blur' },
    { type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
  ]
};

是不是几乎一样!

第 3 步:在 Controller 使用

@Post('/user')
createUser(
  @Body(new ValidationPipe()) body: CreateUserDto
) {
  return body;
}

第 4 步:验证不通过会怎样?

// 请求体
{
  "username": "ab",  // 长度不够!
  "age": -5,         // 负数!
  "email": "invalid" // 邮箱格式不对!
}

返回 400 错误:

{
  "statusCode": 400,
  "message": [
    "username must be longer than or equal to 3 characters",
    "age must not be less than 0",
    "email must be an email"
  ],
  "error": "Bad Request"
}

4. 核心原理:class-validator + class-transformer

class-transformer:普通对象 → Class 实例

import { plainToInstance } from 'class-transformer';

const dto = plainToInstance(CreateUserDto, { username: 'abc' });
// 普通的 { username: 'abc' } → CreateUserDto 实例

class-validator:验证装饰器

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

class CreateUserDto {
  @IsString()        // 必须是字符串
  @MinLength(3)      // 最小长度 3
  username: string;

  @IsInt()           // 必须是整数
  @Min(0)            // 最小值 0
  @Max(150)          // 最大值 150
  age: number;

  @IsEmail()         // 必须是邮箱格式
  email: string;
}

对比前端 Element Plus:

// rules 配置
const rules = {
  username: [
    { required: true, message: '用户名不能为空', trigger: 'blur' },
    { min: 3, message: '用户名至少3个字符', trigger: 'blur' }
  ],
  age: [
    { required: true, message: '年龄不能为空', trigger: 'blur' },
    { type: 'number', min: 0, max: 150, message: '年龄范围 0-150', trigger: 'blur' }
  ],
  email: [
    { required: true, message: '邮箱不能为空', trigger: 'blur' },
    { type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
  ]
};

5. 常用验证装饰器一览

Nest 装饰器作用Element Plus rules 对比
@IsString()字符串{ type: 'string' }
@IsInt()整数{ type: 'integer' }
@IsEmail()邮箱{ type: 'email' }
@IsUrl()URL{ type: 'url' }
@MinLength(n)最小长度{ min: n, message: '...' }
@MaxLength(n)最大长度{ max: n, message: '...' }
@Min(n)最小值{ min: n, message: '...' }
@Max(n)最大值{ max: n, message: '...' }
@IsArray()数组{ type: 'array' }
@IsEnum()枚举{ type: 'enum' }
@IsOptional()可选required: false

6. 自定义错误消息

@MinLength(3, {
  message: '用户名至少3个字符'
})
@Max(150, {
  message(args) {
    return `年龄不能超过 ${args.constraints[0]}`;
  }
})
age: number;

对比前端 Element Plus:

// rules 配置
const rules = {
  age: [
    { required: true, message: '年龄不能为空', trigger: 'blur' },
    { type: 'number', min: 0, message: '年龄不能为负数', trigger: 'blur' },
    { type: 'number', max: 150, message: '年龄不能超过150', trigger: 'blur' }
  ]
};

7. 全局验证?一键启用!

每次手动加 new ValidationPipe() 太麻烦?

// main.ts
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,  // 自动过滤多余字段
  transform: true  // 自动转换类型
}));

whitelist 的作用:

// 请求体有多余字段
{
  "username": "abc",
  "age": 18,
  "hacker": "trying to break in"  // 这个会被过滤掉!
}

8. 完整流程图

Post 请求进来
   │
   ▼
┌──────────────────────────────────────┐
│  1. class-transformer                 │
│  普通对象 → DTO Class 实例            │
└──────────────────────────────────────┘
   │
   ▼
┌──────────────────────────────────────┐
│  2. class-validator                   │
│  基于装饰器验证每一个字段              │
└──────────────────────────────────────┘
   │
   ▼
┌──────────────────────────────────────┐
│  验证通过 → Controller 继续处理        │
│  验证失败 → 返回 400 错误             │
└──────────────────────────────────────┘

9. 总结

概念前端类比作用
DTOel-form接收请求体数据
class-validatorel-form rules声明式验证规则
class-transformer-对象类型转换
ValidationPipe表单验证组件管道验证

一句话总结

ValidationPipe 就是后端的表单验证器,用声明式(装饰器)来定义验证规则,和前端的 Element Plus el-form 几乎一模一样。