首页 / NestJS 入门教程 / 管道 Pipe

NestJS 入门教程

管道 Pipe

本教程共 47 篇 · 第 16 篇 · 更新于 2026-08-09 · 约 11 分钟阅读

NestJSPipe管道ValidationPipe数据验证数据转换class-validator

本节目标:理解管道的两个核心作用——转换和验证,学会用内置管道处理类型转换,掌握 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 前面。因为如果参数是 undefinedParseIntPipe 会直接报错。先给默认值,再转换类型,顺序不能反。

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 管道的完整知识:

  • 管道做两件事:转换数据格式、验证数据合法性
  • 内置管道覆盖常见场景:ParseIntPipeParseUUIDPipeDefaultValuePipe
  • ValidationPipe 配合 class-validator 实现声明式验证
  • whitelisttransformforbidNonWhitelisted 三个配置项建议开启
  • 自定义管道实现 PipeTransform 接口的 transform 方法
  • 管道可以在参数、方法、控制器、全局四个层级绑定
  • 管道在请求生命周期中位于守卫之后、控制器之前

到这里,NestJS 的核心概念就全部讲完了。从模块、控制器、提供者,到依赖注入、中间件、异常处理、管道,你已经掌握了构建 NestJS 应用的基础知识。接下来就是动手写项目,在实践中加深理解。