CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
NestJS 实战入门/课时/Pipe 与校验
RawBlame
课时目录12 节

NestJS 实战入门

  1. 01NestJS 是什么
  2. 02项目创建与结构
  3. 03Module 与依赖注入
  4. 04Controller 与路由
  5. 05Service 层
  6. 06Pipe 与校验
  7. 07Guard 与认证
  8. 08ConfigModule
  9. 09数据库接入入门
  10. 10异常与过滤器
  11. 11测试
  12. 12综合练习

Pipe 与校验

ValidationPipe、class-validator、转换与 whitelist。

35 分钟

Pipe 与校验

这一课你将能做到

  1. 全局启用 ValidationPipe,配置 whitelist / forbidNonWhitelisted / transform
  2. 用 class-validator 装饰器约束题目 DTO,非法请求体直接得到 400
  3. 用 ParseIntPipe、ParseEnumPipe 等把路径与查询参数安全转换

为什么要学

Pipe(管道)人话:请求到达 Controller 方法之前,先做一次「转换」或「校验」。

最常用的是全局 ValidationPipe:它依据 DTO 上的装饰器检查 JSON——字段缺失、类型错误、选项数组为空、answerIndex 不是数字,都会被直接拦下并返回 400,业务方法根本看不到脏数据。这等于在每个接口入口统一设了一道关卡,而不是在每个方法里手写一堆 if。

它对应 Spring 的 @Valid 加 Bean Validation(JSR-303):规则声明在字段上,框架负责执行。

先建立图像

HTTP JSON
  → ValidationPipe(whitelist / forbid / transform)
  → 合法:plain object 被构造成 CreateQuestionDto 实例
  → QuestionsController.create(dto)
  → QuestionsService

非法 → 400 + 字段级错误信息(进不了业务)

三个核心选项:

选项人话解释
whitelist: true自动剥掉 DTO 里没声明的字段
forbidNonWhitelisted: true一旦多传未知字段,直接拒绝(400)
transform: true把普通对象转成 DTO 类实例,并按参数类型做转换

跟着做

步骤 1:安装校验依赖

npm install class-validator class-transformer

class-validator 提供规则装饰器,class-transformer 负责把 JSON 对象转换成类实例,二者是 Nest 官方校验文档的标准搭档。

步骤 2:全局启用 ValidationPipe

修改 src/main.ts:

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix("api");
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

此后所有以 class 接收的 @Body() 都会自动校验。

步骤 3:给 CreateQuestionDto 加约束

替换 src/questions/dto/create-question.dto.ts:

import {
  ArrayMinSize,
  IsArray,
  IsEnum,
  IsInt,
  IsOptional,
  IsString,
  Max,
  MaxLength,
  Min,
  MinLength,
} from "class-validator";

export class CreateQuestionDto {
  @IsString()
  @MinLength(4, { message: "题干至少 4 个字符" })
  @MaxLength(500)
  stem: string;

  @IsArray()
  @ArrayMinSize(2, { message: "至少提供 2 个选项" })
  @IsString({ each: true })
  options: string[];

  @IsInt()
  @Min(0)
  @Max(7)
  answerIndex: number;

  @IsEnum(["easy", "medium", "hard"], {
    message: "difficulty 只能是 easy、medium、hard",
  })
  difficulty: "easy" | "medium" | "hard";

  @IsOptional()
  @IsArray()
  @IsString({ each: true })
  tags?: string[];
}

先发一组合法请求确认 201,再发非法请求:

curl -i -X POST http://localhost:3000/api/questions -H "Content-Type: application/json" -d "{\"stem\":\"x\",\"options\":[],\"answerIndex\":9,\"difficulty\":\"unknown\",\"hacked\":true}"

应得到 400,响应体 message 是一个数组,逐条列出哪个字段违反了什么规则;未知字段 hacked 也会被拒绝。

步骤 4:验证 whitelist 的剥离行为

临时把 forbidNonWhitelisted 改为 false,只保留 whitelist: true,同样多传 hacked。请求可能成功,但进入 Service 的 DTO 里没有这个字段——它被静默剥离了。

生产 API 建议保持 forbidNonWhitelisted: true:让调用方立刻知道自己传了接口不认的字段,而不是埋下「以为生效了其实被忽略」的坑。验证完改回 true。

步骤 5:参数级管道转换数字与枚举

更新 QuestionsController:

import {
  DefaultValuePipe,
  Get,
  ParseEnumPipe,
  ParseIntPipe,
  Query,
} from "@nestjs/common";

@Get()
findAll(
  @Query("page", new DefaultValuePipe(1), ParseIntPipe) page: number,
  @Query("difficulty", new ParseEnumPipe(["easy", "medium", "hard"]))
  difficulty?: "easy" | "medium" | "hard",
  @Query("tag") tag?: string,
) {
  return this.questions.search({ page, difficulty, tag });
}

@Get(":id")
findOne(@Param("id", ParseIntPipe) id: number) {
  return this.questions.findOne(id);
}

访问以下地址观察结果:

/api/questions?page=abc        → 400(无法转换为整数)
/api/questions?difficulty=xxx  → 400(不在枚举内)
/api/questions/1               → 200,id 已经是 number

注意 ParseEnumPipe 对必填查询参数在缺省时也会报错;若该参数可选,把它做成可选管道或自定义校验(入门阶段可以对 difficulty 直接不用 ParseEnumPipe,交给 DTO 之外的判断)。

步骤 6:给提交 DTO 加校验

新建 src/submissions/dto/submit-answer.dto.ts:

import { IsInt, Max, Min } from "class-validator";

export class SubmitAnswerDto {
  @IsInt()
  @Min(1)
  questionId: number;

  @IsInt()
  @Min(0)
  @Max(7)
  selectedIndex: number;
}

然后让 Controller 使用它。这样空 body、字符串 id、负下标全都在入口被挡住,SubmissionsService 只需要处理判分本身。

步骤 7:写一个自定义转换管道(选做)

新建 src/common/pipes/trim.pipe.ts,实现 PipeTransform 接口:

import { Injectable, PipeTransform } from "@nestjs/common";

@Injectable()
export class TrimPipe implements PipeTransform {
  transform(value: unknown) {
    if (typeof value === "string") {
      return value.trim();
    }
    if (value && typeof value === "object") {
      for (const key of Object.keys(value)) {
        const v = (value as Record<string, unknown>)[key];
        if (typeof v === "string") {
          (value as Record<string, string>)[key] = v.trim();
        }
      }
    }
    return value;
  }
}

用 @UsePipes(new TrimPipe()) 挂在方法上即可去除字符串首尾空白。所有内置管道也都实现同一个 PipeTransform 接口——transform(value, metadata) 返回什么,Controller 拿到的就是什么。

讲清楚要点

  1. 输入不可信,校验必须前置:Pipe 在业务之前执行,脏数据不会扩散到 Service。
  2. 规则声明在 DTO 上:一处声明、所有使用该 DTO 的接口都生效,新增字段只改 DTO。
  3. class 是运行时校验的载体:interface 编译后消失,装饰器需要附着在 class 上。
  4. 多余字段必须处理:whitelist 剥离、forbid 拒绝,避免字段污染和潜在的安全问题。
  5. Query/Param 默认是字符串:用 Parse 系列管道显式转换,转换失败得到 400 而不是 NaN 进入业务。
  6. 格式合法不等于业务合法:校验通过只说明形状对;题干是否重复仍由 Service 查数据决定。

你可能会懵的地方

  • 全局 Pipe 和方法级 Pipe 冲突吗? 初学者常担心二者冲突——不冲突,会叠加执行。全局 ValidationPipe 负责 body 校验;单个参数上的 ParseIntPipe 负责转换,各司其职。

  • transform: true 不开会怎样? @Body() 拿到的是普通对象而不是 DTO 类实例,依赖实例的方法(如 class-transformer 的默认值)不会生效。建议保持开启。

  • 校验信息能用中文吗? 可以,通过 message 自定义;大型项目一般返回错误码,由前端做国际化展示。

  • class-validator 和 Zod 怎么选? class-validator 是 Nest 官方文档路径,装饰器风格统一;Zod 在社区也有对应 Pipe,schema 推导更强。入门先跟官方方案,团队已有 Zod 规范再切换。

  • 为什么全局 pipe 对 @Query 单个参数不自动校验? ValidationPipe 主要针对带 class 装饰器的整体对象;单个标量参数用内置的 Parse 管道处理。

错法对照

错法问题正解
DTO 用 interface运行时没有元数据可校验用 class + 装饰器
只靠 TS 类型挡脏数据类型编译后全部擦除启用 ValidationPipe
Number(page) 不处理 NaN脏分页值进入查询ParseIntPipe
关闭 whitelist 并信任多余字段字段污染、越权赋值风险whitelist + forbid
在每个方法手写 if 校验重复、易漏、难维护规则收敛到 DTO

卡点急救

  1. 校验完全没触发:确认全局 Pipe 注册在 main.ts、DTO 是 class、请求头是 Content-Type: application/json。
  2. each: true 不生效:检查 options 字段确实是数组;装饰器组合顺序应为 @IsArray() 在前、@IsString({ each: true }) 在后。
  3. 枚举查询参数一直 400:ParseEnumPipe 参数应传与 DTO 一致的值列表;可选查询参数不要用强制管道,改用默认值或自定义校验。

面试常问

  • NestJS 的 Pipe 解决什么问题?执行时机是什么? Pipe 在 Guard 之后、Controller 方法调用之前执行,负责参数转换与校验;ValidationPipe 把不合规请求统一拦截为 400,保证业务方法只接收合法数据。

  • whitelist 与 forbidNonWhitelisted 有什么区别? whitelist 只是移除 DTO 未声明的属性;forbidNonWhitelisted 进一步在出现多余属性时直接抛 400。后者对调用方更严格、契约更明确。

  • 对应 Spring 的什么机制? ValidationPipe 加 class-validator 对应 Spring MVC 的 @Valid 加 Hibernate Validator;参数转换对应 Converter / Formatter;都是声明式、在入口执行。

自测三问

  1. whitelist 与 forbidNonWhitelisted 的差别是什么?
  2. 为什么非法 answerIndex 进不了 QuestionsService.create?
  3. ParseIntPipe 解决了查询参数的什么本质问题?

小结一句话

ValidationPipe 加 DTO 装饰器构成入口校验体系,Parse 系列管道负责安全的参数转换。

延伸

  • Validation(官方校验文档)
  • Pipes(管道总览)
  • 下一课预告:实现 Guard 守卫,给题库写操作加上登录与角色控制,对照 Spring Security 理解鉴权链路。
加载本节练习…
← 上一课Service 层下一课 →Guard 与认证