管道 Pipe
本教程共 47 篇 · 第 16 篇 · 更新于 2026-08-09 · 约 11 分钟阅读
本节目标:理解管道的两个核心作用——转换和验证,学会用内置管道处理类型转换,掌握 ValidationPipe 做数据校验,以及自己写管道。
自来水从水厂到你家,中间要经过好几道管道。每道管道负责一件事:过滤、消毒、加压。最终到你水龙头的水,已经是干净可用的了。
NestJS 的管道也是这个意思。数据从客户端进来,经过管道处理后,到达控制器手里的数据已经是转换好格式、验证过合法性的了。
管道做两件事:
- 转换:把数据变成你想要的格式,比如字符串转数字
- 验证:检查数据合不合法,不合法就抛异常,让请求到不了控制器
内置管道速览
NestJS 自带了一批管道,开箱即用:
| 管道 | 作用 |
|---|---|
ParseIntPipe | 转整数 |
ParseFloatPipe | 转浮点数 |
ParseBoolPipe | 转布尔值 |
ParseArrayPipe | 转数组 |
ParseUUIDPipe | 验证 UUID 格式 |
ParseEnumPipe | 验证枚举值 |
DefaultValuePipe | 设置默认值 |
ValidationPipe | 数据验证(配合 class-validator) |
这些管道都从 @nestjs/common 导入。
数据转换
ParseIntPipe
URL 里的参数都是字符串。/users/123 里的 123 是字符串 "123",不是数字。
ParseIntPipe 帮你把它转成数字,转换失败就抛 400 错误:
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// 这里的 id 已经是 number 类型了
return this.usersService.findOne(id);
}
}
如果请求 GET /users/abc,管道会直接抛出异常,返回 400:
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
控制器里的代码根本不会执行。
Tip传管道的时候,可以直接传类(
ParseIntPipe),也可以传实例(new ParseIntPipe())。传类的话,框架负责实例化,支持依赖注入。传实例的话,可以自定义配置。
自定义错误状态码
内置管道支持自定义配置。比如转换失败时返回 404 而不是 400:
@Get(':id')
findOne(
@Param('id', new ParseIntPipe({
errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE,
}))
id: number,
) {
return this.usersService.findOne(id);
}
ParseUUIDPipe
如果你的 ID 用的是 UUID 格式:
import { ParseUUIDPipe } from '@nestjs/common';
@Get(':uuid')
findOne(@Param('uuid', ParseUUIDPipe) uuid: string) {
return this.usersService.findOne(uuid);
}
请求传了个不合法的 UUID,直接返回 400。
Note
ParseUUIDPipe默认支持 v3、v4、v5 三种版本。如果你只需要特定版本,可以传配置:new ParseUUIDPipe({ version: '4' })。
DefaultValuePipe + ParseIntPipe 组合
分页参数经常需要默认值。用户不传 page 就默认第 1 页,不传 limit 就默认 10 条:
import { Query, DefaultValuePipe, ParseIntPipe } from '@nestjs/common';
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
return this.usersService.findAll(page, limit);
}
管道是按顺序执行的。DefaultValuePipe 先跑,给参数一个默认值;然后 ParseIntPipe 再跑,把值转成数字。
Tip
DefaultValuePipe必须放在ParseIntPipe前面。因为如果参数是undefined,ParseIntPipe会直接报错。先给默认值,再转换类型,顺序不能反。
optional 模式
如果参数是可选的,可以设置 optional: true:
@Get(':id')
findOne(
@Param('id', new ParseIntPipe({ optional: true })) id?: number,
) {
// id 可能是 undefined
return id;
}
数据验证
验证是管道最重要的用途。NestJS 内置的 ValidationPipe 配合 class-validator 库,提供了一套非常优雅的验证方案。
安装依赖
npm install class-validator class-transformer
class-validator 提供验证装饰器,class-transformer 负责把普通对象转成类实例。两个包配合使用。
定义 DTO 验证规则
DTO(Data Transfer Object)就是数据传输对象。你在 DTO 上用装饰器标注验证规则:
import {
IsString,
IsEmail,
IsInt,
Min,
Max,
IsOptional,
IsNotEmpty,
MinLength,
MaxLength,
} from 'class-validator';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
@MinLength(2)
@MaxLength(50)
name: string;
@IsEmail()
email: string;
@IsInt()
@Min(0)
@Max(150)
age: number;
@IsOptional()
@IsString()
bio?: string;
}
每个装饰器就是一条验证规则。一个字段可以叠加多个规则。
配置全局验证管道
在 main.ts 中注册 ValidationPipe:
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
这样所有用了 DTO 的接口都会自动验证。
ValidationPipe 的重要配置
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
这三个选项非常实用:
whitelist: true —— 自动剥离 DTO 中没有定义的属性。用户传了 {"name": "Tom", "admin": true},但 DTO 里没有 admin 字段,这个字段会被自动去掉。
forbidNonWhitelisted: true —— 更严格,直接返回 400 错误而不是静默剥离。
transform: true —— 自动把请求体转成 DTO 类的实例,同时做类型转换。比如请求体里的 age: "25"(字符串)会自动变成 age: 25(数字)。
Warning强烈建议开启
whitelist: true。不开的话,用户可以往你的 DTO 里塞任意字段,虽然这些字段不会被使用(因为没有装饰器),但保留它们是个安全隐患。
自定义验证错误消息
默认的英文错误消息不够友好,可以自定义:
import { IsString, IsEmail, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString({ message: '姓名必须是字符串' })
@MinLength(2, { message: '姓名至少 2 个字符' })
name: string;
@IsEmail({}, { message: '邮箱格式不正确' })
email: string;
}
验证失败时返回的就是你定义的消息了。
嵌套对象验证
如果 DTO 里有嵌套对象,需要额外处理:
import { IsString, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
class AddressDto {
@IsString()
street: string;
@IsString()
city: string;
}
export class CreateUserDto {
@IsString()
name: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}
@ValidateNested() 告诉 ValidationPipe 这个字段需要递归验证。@Type(() => AddressDto) 告诉 class-transformer 把嵌套对象转成 AddressDto 实例。
Note嵌套验证是新手常踩的坑。不加
@ValidateNested()和@Type(),嵌套对象里的验证规则不会生效,而且不会报错——只是悄悄跳过了。
数组验证
如果字段是数组,用 each: true 对每个元素验证:
import { IsArray, ValidateNested, IsString } from 'class-validator';
import { Type } from 'class-transformer';
class TagDto {
@IsString()
name: string;
}
export class CreatePostDto {
@IsString()
title: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => TagDto)
tags: TagDto[];
}
PartialType 做更新验证
更新操作通常所有字段都是可选的。不用重新写一个 DTO,用 PartialType 就行:
import { PartialType } from '@nestjs/mapped-types';
export class UpdateUserDto extends PartialType(CreateUserDto) {
// 所有字段自动变成可选的
}
PartialType 会把 CreateUserDto 里的所有字段标记为 @IsOptional()。
自定义管道
内置管道不够用的时候,可以自己写。
管道接口
每个管道都要实现 PipeTransform 接口的 transform 方法:
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class MyPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
// 处理 value,返回处理后的值
return value;
}
}
transform 方法接收两个参数:
value:当前参数的值metadata:参数的元信息,包含type(body/query/param)、metatype(参数的类型)、data(装饰器里传的字符串)
手写 ParseIntPipe
看看内置的 ParseIntPipe 到底做了什么:
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('转换失败:不是合法的数字');
}
return val;
}
}
逻辑很简单:用 parseInt 转,转不了就抛异常。
Note管道里抛出的异常会被 NestJS 的异常处理层捕获。所以你可以放心地
throw,不用自己处理响应。
手写验证管道
如果你想了解 ValidationPipe 内部的工作原理,看看这个简化版:
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class CustomValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
// 如果没有 metatype,或者 metatype 是内置类型,跳过验证
if (!metatype || !this.toValidate(metatype)) {
return value;
}
// 把普通对象转成类实例
const object = plainToInstance(metatype, value);
// 执行验证
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException({
message: '验证失败',
errors: errors.map(error => ({
property: error.property,
constraints: error.constraints,
})),
});
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
关键点在于 plainToInstance。HTTP 请求体反序列化后只是一个普通的 JavaScript 对象,没有类信息。class-validator 的装饰器是挂在类原型上的,所以必须先转成类实例才能验证。
Tip实际项目中直接用内置的
ValidationPipe就好。这里手写一遍是为了帮你理解原理。
实用管道:TrimPipe
去掉字符串两端的空格,听起来简单,但每个接口都写一遍就烦了。写成管道一劳永逸:
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class TrimPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
if (typeof value === 'string') {
return value.trim();
}
if (typeof value === 'object' && value !== null) {
return this.trimObject(value);
}
return value;
}
private trimObject(obj: any): any {
const result: any = {};
for (const key in obj) {
if (typeof obj[key] === 'string') {
result[key] = obj[key].trim();
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
result[key] = this.trimObject(obj[key]);
} else {
result[key] = obj[key];
}
}
return result;
}
}
实用管道:从 ID 查实体
管道不只是验证和类型转换,还可以做数据查询。比如根据 ID 从数据库查出实体,直接传给控制器:
import { PipeTransform, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UserByIdPipe implements PipeTransform<string, Promise<User>> {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async transform(value: string): Promise<User> {
const user = await this.usersRepository.findOne({ where: { id: value } });
if (!user) {
throw new NotFoundException(`用户 ${value} 不存在`);
}
return user;
}
}
用的时候:
@Get(':id')
findOne(@Param('id', UserByIdPipe) user: User) {
// 这里的 user 已经是查出来的实体了
return user;
}
控制器代码变得更干净,查数据库的逻辑被封装到了管道里。
管道的作用范围
管道可以在四个层级绑定:
参数级别 —— 只对一个参数生效:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {}
方法级别 —— 对方法里所有参数生效:
@Post()
@UsePipes(ValidationPipe)
create(@Body() createUserDto: CreateUserDto) {}
控制器级别 —— 对控制器里所有方法生效:
@Controller('users')
@UsePipes(ValidationPipe)
export class UsersController {}
全局级别 —— 对所有接口生效:
app.useGlobalPipes(new ValidationPipe());
Tip大多数项目只需要一个全局
ValidationPipe就够了。个别参数需要特殊处理时,再在参数级别绑定额外的管道。
用 APP_PIPE 注册全局管道
跟异常过滤器类似,useGlobalPipes 注册的管道没法注入依赖。如果需要依赖注入,用 APP_PIPE token:
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
管道在请求生命周期中的位置
请求 → 中间件 → 守卫 → 拦截器(前) → 管道 → 控制器 → 拦截器(后) → 响应
管道在守卫和拦截器之间执行。这意味着:
- 守卫先做权限检查
- 然后管道做数据验证和转换
- 最后控制器拿到的是干净的、验证过的数据
Note管道抛出的异常会被异常处理层捕获,控制器不会执行。这就是”在系统边界做验证”的思想——脏数据根本到不了业务逻辑层。
本章小结
这一章讲了 NestJS 管道的完整知识:
- 管道做两件事:转换数据格式、验证数据合法性
- 内置管道覆盖常见场景:
ParseIntPipe、ParseUUIDPipe、DefaultValuePipe等 ValidationPipe配合class-validator实现声明式验证whitelist、transform、forbidNonWhitelisted三个配置项建议开启- 自定义管道实现
PipeTransform接口的transform方法 - 管道可以在参数、方法、控制器、全局四个层级绑定
- 管道在请求生命周期中位于守卫之后、控制器之前
到这里,NestJS 的核心概念就全部讲完了。从模块、控制器、提供者,到依赖注入、中间件、异常处理、管道,你已经掌握了构建 NestJS 应用的基础知识。接下来就是动手写项目,在实践中加深理解。