本文属于 NestJS BBS 实战教程,共 11 节,建议按顺序学习。
1. 这节课要实现的功能是什么
- 把用户创建接口从“裸对象接收”升级为“受约束输入”。
- 开启全局
ValidationPipe,让非法请求在入口就被拦截。 - 理解为什么校验要放在 Controller 入口层,而不是分散在 Service。
2. 这节课涉及的 NestJS 知识点有哪些(先讲概念)
2.1 先理解 Pipe:它到底是什么
Pipe 是 Nest 的“参数处理器”。它会在 Controller 方法执行前,对参数做处理。
常见动作有三类:转换类型、校验格式、拒绝非法输入。
所以你可以把 Pipe 理解成 API 入口第一道“数据闸门”。
2.2 再看 ValidationPipe:为什么需要它
ValidationPipe 是最常用的 Pipe,专门负责请求参数校验。
没有它,DTO 上的 class-validator 规则不会自动执行。
它解决的是一个实际问题:
前端请求不可信,后端必须在入口统一拦截错误数据,而不是把校验散落在 Service 的 if 里。
2.3 DTO 是什么,为什么必须用 class
DTO(Data Transfer Object)是“输入契约”。
ValidationPipe 会读取 DTO 上的装饰器元数据来做校验。
因为 TypeScript 的 interface 在编译后会被擦除,运行时拿不到元数据,所以 DTO 必须用 class:
import { IsEmail, IsString, MaxLength, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(3)
@MaxLength(30)
username!: string;
@IsEmail()
@MaxLength(255)
email!: string;
@IsString()
@MinLength(8)
@MaxLength(72)
password!: string;
}
2.4 ValidationPipe 在请求链路里的执行位置
回答你前面的问题:会先过 Pipe,再进 Controller。
DTO 不是“先执行的一段代码”,而是 Pipe 在运行时读取的规则对象。
请求大致按这条顺序流转:
- 请求命中路由;
- Nest 读取参数元数据(
@Body()、@Param()、DTO 类型); - 执行对应 Pipe(含全局、方法级、参数级);
- 全部通过后,才进入 Controller 方法体。
2.5 只写 @Body() dto: CreateUserDto 会自动校验吗?
不会。这个写法本身只提供类型提示,不会触发运行时校验。
如果没有配置 ValidationPipe,一般不会报 400,非法数据会直接流入业务层。
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
2.6 如何启用 ValidationPipe(三种方式)
你可以按作用范围选择:
- 全局:
useGlobalPipes,整个应用生效; - 方法/控制器级:
@UsePipes(...),局部生效; - 参数级:
@Body(new ValidationPipe()),仅单个参数生效。
@Post()
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }))
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
2.7 ValidationPipe 的核心配置项
whitelist: true:删除 DTO 未声明字段;forbidNonWhitelisted: true:出现额外字段直接报错;transform: true:按目标类型做转换(例如字符串转数字)。
这三个配置组合,是大多数线上 API 的基础安全配置。
2.8 常用内置 Pipe 与多 Pipe 组合
除了 ValidationPipe,常用内置 Pipe 还有:
ParseIntPipe:字符串转整数;ParseBoolPipe:字符串转布尔;ParseUUIDPipe:校验 UUID;ParseArrayPipe:处理数组参数;DefaultValuePipe:设置默认值。
多个 Pipe 可以叠加使用,参数级会按声明顺序执行:
@Get(':id')
findOne(@Param('id', ParseIntPipe, UserExistsPipe) id: number) {
return this.usersService.findOneById(id);
}
这里会先把 id 转成数字,再校验用户是否存在。
2.9 APP_PIPE 是什么,为什么会有它
APP_PIPE 是 Nest 的全局 Pipe 注入令牌。
把 Provider 绑定到 APP_PIPE 后,这个 Pipe 会全局生效,并且仍然在 DI 容器里。
它存在的原因很直接:app.useGlobalPipes(new XxxPipe()) 虽然简单,但不适合需要依赖注入的全局 Pipe。
典型场景:
- 全局 Pipe 需要注入
ConfigService; - 全局 Pipe 需要注入业务 Service;
- 你希望全局 Pipe 与其他 Provider 一样统一由模块管理。
2.10 自定义 Pipe 一定要用 APP_PIPE 吗?
不需要。APP_PIPE 只是一种“全局注册方式”,不是自定义 Pipe 的必选项。
- 只在单个接口使用:参数级或
@UsePipes即可; - 需要全局生效且有依赖注入:优先
APP_PIPE; - 纯无依赖、快速全局启用:
useGlobalPipes就够用。
2.11 有依赖的 Provider,为什么要关注可见性
只要一个 Provider 有依赖,就必须确保依赖链可被 DI 容器解析。
不要求全部写在同一个模块,但必须能通过 providers + imports/exports 链路找到。
你可以按这条规则检查:
- 当前类是否注册到某个模块
providers; - 它依赖的类是否也已注册;
- 跨模块时,提供方是否
exports,使用方是否imports。
2.12 常见误区
- 误区 1:只做前端校验,不做后端校验;
- 误区 2:把校验逻辑全部写进 Service;
- 误区 3:DTO 没有长度/格式约束,导致脏数据进入系统。
3. 实际开发中这些 NestJS 知识点被如何使用(详细代码)
3.1 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.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.listen(3000);
}
bootstrap();
3.2 src/users/dto/create-user.dto.ts - 定义输入契约
import { IsEmail, IsString, MaxLength, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(3)
@MaxLength(30)
username!: string;
@IsEmail()
@MaxLength(255)
email!: string;
@IsString()
@MinLength(8)
@MaxLength(72)
password!: string;
}
3.3 src/users/users.controller.ts - 在入口层使用 DTO 与 Pipe
import { Body, Controller, Get, Param, ParseIntPipe, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
async create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOneById(id);
}
}
3.4 多个 Pipe 同时使用(方法级 + 参数级)
import { Controller, Get, Param, ParseIntPipe, UsePipes, ValidationPipe } from '@nestjs/common';
import { UserExistsPipe } from './pipes/user-exists.pipe';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
@UsePipes(new ValidationPipe({ transform: true }))
findOne(@Param('id', ParseIntPipe, UserExistsPipe) id: number) {
return this.usersService.findOneById(id);
}
}
3.5 依赖注入场景下的全局 Pipe(APP_PIPE 示例)
当 Pipe 需要读取配置或注入服务时,推荐在模块里注册:
import { Module, ValidationPipe } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
@Module({
providers: [
{
provide: APP_PIPE,
inject: [ConfigService],
useFactory: (config: ConfigService) =>
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: config.get('NODE_ENV') === 'production',
}),
},
],
})
export class AppModule {}
3.6 UserExistsPipe 示例(带依赖注入)
先定义 Pipe:
import { Injectable, PipeTransform } from '@nestjs/common';
import { UsersService } from '../users.service';
@Injectable()
export class UserExistsPipe implements PipeTransform<number, Promise<number>> {
constructor(private readonly usersService: UsersService) {}
async transform(value: number): Promise<number> {
await this.usersService.findOneById(value);
return value;
}
}
再在 Controller 参数上使用:
@Get(':id')
findOne(@Param('id', ParseIntPipe, UserExistsPipe) id: number) {
return this.usersService.findOneById(id);
}
最后确保依赖链可见(示例):
@Module({
imports: [UsersModule],
providers: [UserExistsPipe],
})
export class ThreadsModule {}
3.7 你可以这样验证效果
# 合法请求
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"username":"tom","email":"tom@example.com","password":"password123"}'
# 非法请求(email 不合法 + 多余字段 role)
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"username":"tom","email":"bad","password":"12345678","role":"admin"}'
你会看到后端在入口阶段直接拒绝,而不是把脏数据送进 Service。
4. 总结这节课知识点
- DTO 是接口契约,负责把输入规则集中管理。
ValidationPipe在进入 Controller 前执行,DTO 是它读取规则的载体。- 你掌握了多个 Pipe 的叠加方式与执行顺序。
- 你知道了
useGlobalPipes与APP_PIPE的选型边界。 UserExistsPipe让“资源存在性校验”前置,减少重复业务代码。- 你理解了有依赖 Provider 的可见性规则(providers + imports/exports)。
- 从这节课开始,你的 API 已具备基础“可维护 + 可防御”能力。
5. 每节课分支
lesson-02-dto-validation- 远程仓库:
https://github.com/Shiuwn/nestjs-tutorial.git