Skip to content

NestJS 管道

目录

管道

管道(Pipes)是用 @Injectable() 装饰器标注的类,实现了 PipeTransform 接口,就像流水线上的“加工站”,对输入数据进行处理或校验。管道有两个典型用途:

  1. 转型(Transformation):将输入数据转换为所需格式,比如把字符串 “123” 变成整数 123。
  2. 验证(Validation):检查输入数据是否合法,合格的直接放行,不合格的抛出异常。

管道作用于控制器路由处理器(即控制器方法)接收的参数,在方法执行前运行。NestJS 会先将参数交给管道处理,管道对参数进行转换或验证,处理后的结果(或抛出的异常)再传递给路由处理器。

NestJS 内置了许多开箱即用的管道,你也可以打造自己的自定义管道。

提示:管道运行在“异常区域”(exception zone)内。如果管道抛出异常,会由异常层(全局异常过滤器或当前上下文的异常过滤器)处理。因此,管道抛出异常后,控制器方法不会被执行。这为你在系统边界验证外部输入数据提供了一种最佳实践,就像在“入口关卡”过滤掉不合格的数据。

以下是管道的核心接口 PipeTransform 的定义,带注释说明其作用:

typescript
import { ArgumentMetadata } from '@nestjs/common';

// PipeTransform 接口定义了管道的转型/验证逻辑
export interface PipeTransform<T = any, R = any> {
  transform(value: T, metadata: ArgumentMetadata): R;
}

代码解释

  • @Injectable():管道类需要用此装饰器标记,使其可被 NestJS 的 DI 容器管理。
  • PipeTransform:所有管道必须实现此接口,定义 transform 方法,接收输入值(value)和元数据(metadata),返回处理后的值或抛出异常。
  • value: T:控制器方法的参数值(如请求的查询参数或 body 数据)。
  • metadata: ArgumentMetadata:包含参数的上下文信息(如参数类型、名称、是否为 body/query 等)。
  • R:转换后的返回值类型,可能是原始值、转换后的值或抛出的异常。

示例:内置管道 ParseIntPipe

NestJS 提供内置的 ParseIntPipe,用于将字符串转换为整数并验证:

typescript
import { Controller, Get, Query } from '@nestjs/common';
import { ParseIntPipe } from '@nestjs/common';

@Controller('example')
export class ExampleController {
  @Get()
  findById(@Query('id', ParseIntPipe) id: number) {
    return { id }; // id 已由 ParseIntPipe 转换为整数
  }
}

解释

  • @Query('id', ParseIntPipe):将查询参数 id(如 "123")交给 ParseIntPipe 处理。
  • ParseIntPipe:检查 id 是否为有效的数字字符串,如果是,转换为整数(如 123);否则抛出 BadRequestException
  • 异常区域:如果 id 不是数字(如 "abc"),ParseIntPipe 抛出异常,交给全局或上下文的异常过滤器处理,控制器方法不会执行。

内置管道

NestJS 提供了一套开箱即用的内置管道,能快速处理常见的转型和验证需求。这些管道包括:

  • ValidationPipe:验证数据对象(如 DTO),常与 class-validator 配合。
  • ParseIntPipe:将字符串转为整数。
  • ParseFloatPipe:将字符串转为浮点数。
  • ParseBoolPipe:将字符串转为布尔值。
  • ParseArrayPipe:将输入转为数组并验证其内容。
  • ParseUUIDPipe:验证 UUID 格式。
  • ParseEnumPipe:验证输入是否属于指定枚举值。
  • DefaultValuePipe:为缺失的参数提供默认值。
  • ParseFilePipe:验证上传的文件。
  • ParseDatePipe:将字符串转为日期对象。

这些管道都从 @nestjs/common 包中导出,方便直接使用。

让我们以 ParseIntPipe 为例,快速了解它的用法。这是一个典型的转型场景:管道确保控制器方法的参数被转换为 JavaScript 整数,如果转换失败则抛出异常。 Parse* 管道,包括 ParseBoolPipeParseFloatPipeParseEnumPipeParseArrayPipeParseDatePipeParseUUIDPipe

以下是 ParseIntPipe 的使用示例,展示如何将查询参数转换为整数:

typescript
import { Controller, Get, Query } from '@nestjs/common';
import { ParseIntPipe } from '@nestjs/common';

@Controller('example')
export class ExampleController {
  @Get()
  findById(@Query('id', ParseIntPipe) id: number) {
    // id 已被 ParseIntPipe 转换为整数
    return { id };
  }
}

代码解释

  • @Query('id', ParseIntPipe):将查询参数 id(如 "123")交给 ParseIntPipe 处理。
  • ParseIntPipe:尝试将输入字符串转换为整数(通过 parseInt)。如果输入有效(如 "123"),返回整数 123;如果无效(如 "abc"),抛出 BadRequestException
  • 异常区域:抛出的异常会被异常过滤器(如全局过滤器)捕获,控制器方法不会执行。
  • 类型安全id: number 确保 TypeScript 编译时检查参数类型,管道在运行时保证实际值符合类型。

ParseIntPipe 的内置逻辑(伪代码)

以下是 ParseIntPipe 的简化实现,展示其工作原理:

typescript
import { PipeTransform, BadRequestException } from '@nestjs/common';

export class ParseIntPipe implements PipeTransform {
  transform(value: any): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed: id must be a number');
    }
    return val;
  }
}

解释

  • transform:接收输入值,尝试用 parseInt 转换为整数。
  • 验证:如果结果为 NaN(如输入 "abc"),抛出异常。
  • 转型:返回转换后的整数。

绑定管道

要使用管道(Pipes),我们需要将管道类的实例绑定到合适的上下文,确保在进入控制器方法前对参数进行转型或验证。以 ParseIntPipe 为例,我们希望将它绑定到特定的路由处理程序(控制器方法),确保方法执行前参数被正确处理。NestJS 提供了一种方法参数级别的绑定方式,通过装饰器(如 @Param@Query)应用管道:

typescript
import { Controller, Get, Param, Query } from '@nestjs/common';
import { ParseIntPipe, HttpStatus } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    return this.catsService.findOne(id); // id 已转为整数
  }
}

这种绑定方式确保以下两者之一成立:

  1. 控制器方法(如 findOne)接收到的参数是预期类型(例如整数 id),符合 catsService.findOne 的要求。
  2. 如果参数无效(例如非数字字符串),管道抛出异常,阻止方法执行。

示例场景: 假设请求为 GET localhost:3000/abcParseIntPipe 会抛出以下异常:

json
{
  "statusCode": 400,
  "message": "Validation failed (numeric string is expected)",
  "error": "Bad Request"
}

这个异常会阻止 findOne 方法执行,确保只有有效数据进入业务逻辑。

在上面的代码中,我们传递了管道类(ParseIntPipe)而非实例,将实例化责任交给 NestJS 框架,启用依赖注入(DI)。管道和守卫类似,也可以传递就地实例,以便自定义行为。例如,修改 ParseIntPipe 的错误状态码:

typescript
@Get(':id')
async findOne(
  @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
  id: number,
) {
  return this.catsService.findOne(id);
}

这种绑定方式同样适用于其他 Parse* 管道(如 ParseBoolPipeParseUUIDPipe 等),它们都能处理路由参数、查询参数或请求体。以下是查询参数的例子:

typescript
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}

再看一个使用 ParseUUIDPipe 的例子,确保参数是有效的 UUID:

typescript
import { ParseUUIDPipe } from '@nestjs/common';

@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
  return this.catsService.findOne(uuid); // uuid 已验证为有效 UUID
}

提示ParseUUIDPipe 默认验证版本 3、4 或 5 的 UUID。如果需要特定版本,可以通过选项指定:

typescript
@Param('uuid', new ParseUUIDPipe({ version: '4' }))
uuid: string;

绑定 Parse* 管道的方式类似,主要用于转型和验证。ValidationPipe 的绑定稍有不同。

提示:更多 ValidationPipe 的详细示例,请参阅 验证技术

定制管道

除了使用 NestJS 提供的内置管道(如 ParseIntPipeValidationPipe),你还可以从头打造自定义管道。虽然内置管道已经很强大,但通过构建自定义版本,我们可以深入理解管道的实现原理。本节将从一个简单的 ValidationPipe 开始,逐步展示如何创建自定义管道。

我们先构建一个最简单的 ValidationPipe,它像一个“中转站”,接收输入值并直接返回,不做任何处理,类似数学中的恒等函数(输入什么,输出什么):

typescript
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value; // 直接返回输入值,未做处理
  }
}

提示PipeTransform<T, R> 是一个泛型接口,所有管道必须实现。它用 T 表示输入值 value 的类型,用 R 表示 transform 方法的返回类型。

每个管道都必须实现 transform 方法,以满足 PipeTransform 接口的契约。这个方法接收两个参数:

  • value:当前处理的控制器方法参数(在路由处理程序接收前)。
  • metadata:参数的元数据,描述参数的上下文信息。

元数据对象(ArgumentMetadata)包含以下属性:

typescript
export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom'; // 参数来源
  metatype?: Type<unknown>; // 参数的类型(如 String、Number)
  data?: string; // 装饰器中传递的字符串(如 @Body('key') 的 key)
}

元数据属性解释

  • type:指示参数来源,例如 @Body()(请求体)、@Query()(查询参数)、@Param()(路由参数)或 @Custom()(自定义参数)。
  • metatype:参数的 TypeScript 类型(如 StringNumber)。如果未声明类型或使用普通 JavaScript,值为 undefined
  • data:装饰器中传递的字符串,例如 @Body('key')key。如果装饰器括号为空(如 @Body()),则为 undefined

警告:TypeScript 的接口(interface)在编译为 JavaScript 后会被完全移除,运行时无法获取接口的任何类型信息。因此,如果你的方法参数使用接口类型,NestJS 在运行时获取到的 metatype 只会是 Object,无法精确反映原本的类型结构。

如何解决? 如果你需要在运行时进行类型检查、验证或元数据反射(如在自定义管道或 ValidationPipe 中),建议使用 类(class) 来定义 DTO(数据传输对象),而不是接口。因为类在编译后依然存在于 JavaScript 代码中,NestJS 可以通过反射机制获取到类的构造函数和类型信息。

基于模式的验证

让我们的验证管道更有用一些。仔细查看 CatsControllercreate() 方法,我们可能希望在尝试运行我们的服务方法之前确保请求体对象有效。

接下来,我们关注基于模式的验证。以 CatsControllercreate 方法为例:

typescript
  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto); // createCatDto 需验证
  }

CreateCatDto 定义如下:

typescript
export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

我们需要确保 createCatDto 的请求体有效(例如,namebreed 是非空字符串,age 是正整数)。直接在控制器方法中验证会违反单一职责原则(SRP),增加代码复杂性。另一种方法是创建一个验证器类,但需要在每个方法开头手动调用,容易遗漏。

中间件可能看似是个选择,但它无法访问执行上下文(如控制器方法或参数),因此不适合通用验证。而管道正是为此设计的:它们在方法调用前处理参数,完美适合验证 createCatDto

对象模式验证

对象模式验证(Schema-Based Validation)是一种以干净、DRY(Don't Repeat Yourself)方式验证复杂对象(如 DTO)的方法。相比在控制器中手动验证,使用基于模式的验证可以让代码更简洁、可复用。Zod 是一个强大的模式验证库,API 直观,适合与 NestJS 的管道机制结合。本节将展示如何使用 Zod 创建一个自定义验证管道,专注于简单性和可重用性。

DRY(Don't Repeat Yourself) 是软件开发中的一个重要原则,意思是:不要重复自己,即相同的逻辑只写一次,重复的代码抽出来。

首先,安装 Zod 库:

bash
npm install --save zod

以下代码展示了一个基于 Zod 的验证管道 ZodValidationPipe,它通过构造函数接收一个 Zod 模式(ZodSchema),并使用 schema.parse() 方法验证输入参数。验证管道要么返回通过验证的值,要么抛出异常。

typescript
import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';

@Injectable()
export class ZodValidationPipe implements PipeTransform {
  constructor(private schema: ZodSchema) {}

  transform(value: unknown, metadata: ArgumentMetadata) {
    try {
      const parsedValue = this.schema.parse(value); // 使用 Zod 模式验证
      return parsedValue; // 返回验证后的值
    } catch (error) {
      throw new BadRequestException('Validation failed'); // 验证失败抛出异常
    }
  }
}

代码解释

  • @Injectable():使管道可被 NestJS 的依赖注入(DI)容器管理。
  • constructor(private schema: ZodSchema):接收一个 Zod 模式,定义验证规则。
  • transform:使用 schema.parse() 验证输入值 value,成功返回解析后的值,失败抛出 BadRequestException
  • 异常区域:抛出的异常会被异常过滤器捕获,阻止控制器方法执行。

Zod 模式定义

先定义一个 Zod 模式,用于验证 CreateCatDto

typescript
import { z } from 'zod';

// 定义 Zod 模式
export const CreateCatSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  age: z.number().int().min(1, 'Age must be a positive integer'),
  breed: z.string().nonempty('Breed cannot be empty'),
});

// 类型推导(可选,用于 TypeScript 类型安全)
export type CreateCatDto = z.infer<typeof CreateCatSchema>;

说明

  • z.object:定义对象模式的结构。
  • z.string().min(2):验证 name 是字符串且长度至少 2。
  • z.number().int().min(1):验证 age 是正整数。
  • z.string().nonempty():验证 breed 是非空字符串。

使用 ZodValidationPipe

绑定管道到控制器方法:

typescript
import { Controller, Post, Body } from '@nestjs/common';
import { ZodValidationPipe } from './zod-validation.pipe';
import { CreateCatSchema, CreateCatDto} from './create-cat.schema';

@Controller('cats')
export class CatsController {
  @Post()
  async create(
    @Body(new ZodValidationPipe(CreateCatSchema)) createCatDto: CreateCatDto,
  ) {
    return { cat: createCatDto }; // createCatDto 已通过 Zod 验证
  }
}

测试

http
POST /cats { "name": "Fluffy", "age": 2, "breed": "Persian" }

验证通过,返回 { cat: { name: "Fluffy", age: 2, breed: "Persian" } }

http
POST /cats { "name": "F", "age": -1, "breed": "" }

抛出 BadRequestException,错误消息为 "Validation failed"

http
POST /cats { "name": "Fluffy", "age": "2", "breed": "Persian" }

Zod 自动将 "2" 转为 2z.number() 的行为)。

核心功能

  • ZodValidationPipe:通过 Zod 模式验证输入数据,确保符合定义的规则(如类型、长度、范围)。
  • 可重用性:通过构造函数传递不同 Zod 模式,管道可用于多种 DTO 验证。
  • 异常处理:验证失败抛出 BadRequestException,由异常过滤器捕获,保护控制器方法。

绑定验证管道

我们之前学习了如何绑定转换管道(如 ParseIntPipe 和其他 Parse* 管道),它们专注于将输入转为特定类型(如字符串到整数)。绑定验证管道(如 ZodValidationPipe)同样简单,但通常在方法级别而非参数级别应用,适合验证复杂的对象(如请求体的 DTO)。将 ZodValidationPipe 绑定到控制器方法,确保 CreateCatDto 在进入业务逻辑前通过 Zod 模式验证。

绑定 ZodValidationPipe 的步骤:

  1. 创建 ZodValidationPipe 的实例。
  2. 在管道构造函数中传递上下文特定的 Zod 模式(schema)。
  3. 使用 @UsePipes() 装饰器将管道绑定到方法。

Zod 模式示例

typescript
import { z } from 'zod';

// 定义 Zod 模式
export const createCatSchema = z
  .object({
    name: z.string(),
    age: z.number(),
    breed: z.string(),
  })
  .required(); // 确保所有字段非空

// 推导 TypeScript 类型
export type CreateCatDto = z.infer<typeof createCatSchema>;

说明

  • z.object:定义对象的结构,指定字段类型。
  • z.string()z.number():验证字段类型。
  • .required():确保字段非空(nullundefined 会被拒绝)。
  • z.infer:从模式推导 TypeScript 类型,确保类型安全。

绑定管道

使用 @UsePipes() 装饰器将 ZodValidationPipe 绑定到控制器方法:

typescript
import { Controller, Post, Body, UsePipes } from '@nestjs/common';
import { ZodValidationPipe } from './zod-validation.pipe';
import { CreateCatDto, createCatSchema } from './create-cat.schema';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Post()
  @UsePipes(new ZodValidationPipe(createCatSchema)) // 绑定 ZodValidationPipe
  async create(@Body() createCatDto: CreateCatDto) {
    return this.catsService.create(createCatDto); // createCatDto 已通过验证
  }
}

代码解释

  • @UsePipes(new ZodValidationPipe(createCatSchema)):在方法级别绑定管道,验证整个请求体。
  • createCatDto: CreateCatDto:TypeScript 类型确保编译时安全,Zod 模式在运行时验证。
  • 验证逻辑:ZodValidationPipe 使用 createCatSchema 验证请求体,失败时抛出 BadRequestException

提示@UsePipes()@nestjs/common 导入,用于方法级管道绑定。

警告:使用 Zod 库需在 tsconfig.json 中启用 strictNullChecks 以确保严格的类型检查:

json
{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

增强 ZodValidationPipe 提供详细错误:

typescript
import { PipeTransform, Injectable, BadRequestException, ArgumentMetadata } from '@nestjs/common';
import { ZodSchema } from 'zod';

@Injectable()
export class ZodValidationPipe implements PipeTransform {
  constructor(private schema: ZodSchema) {}

  transform(value: unknown, metadata: ArgumentMetadata) {
    try {
      return this.schema.parse(value);
    } catch (error) {
      // 提取 Zod 错误详情
      throw new BadRequestException(
        error.errors.map((e: any) => e.message).join(', '),
      );
    }
  }
}

开发建议

增强 Zod 模式,添加更严格的验证规则:

typescript
export const createCatSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  age: z.number().int().min(1, 'Age must be a positive integer'),
  breed: z.string().nonempty('Breed cannot be empty'),
});

@UsePipes(new ZodValidationPipe(createCatSchema))@Body(new ZodValidationPipe(createCatSchema)) 的区别

@UsePipes() 在方法级别绑定管道

typescript
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
  return this.catsService.create(createCatDto);
}

作用范围@UsePipes() 是一个方法级装饰器,将管道应用于整个控制器方法,影响方法中所有参数(如 @Body()@Query()@Param() 等)。

验证对象:在例子中,ZodValidationPipe 会被应用到所有参数,但由于方法只有一个 @Body() 参数,实际效果是验证整个请求体 createCatDto

适用场景:适合需要对多个参数统一应用管道的场景,或者当你想为整个方法定义统一的验证逻辑。

@Body(new ZodValidationPipe()) 在参数级别绑定管道

typescript
@Post()
async create(@Body(new ZodValidationPipe(createCatSchema)) createCatDto: CreateCatDto) {
  return this.catsService.create(createCatDto);
}

作用范围@Body(new ZodValidationPipe(createCatSchema)) 是一个参数级装饰器,仅对 @Body() 参数(即请求体 createCatDto)应用管道。

验证对象ZodValidationPipe 只验证请求体,不会影响其他参数(如 @Query()@Param())。

适用场景:适合需要针对特定参数(如请求体)进行验证的场景,更加精确。

总结

@UsePipes():方法级绑定,影响所有参数,适合统一验证逻辑。

@Body(new ZodValidationPipe()):参数级绑定,仅验证请求体,适合精确控制。

在单一 @Body() 场景下,两者效果相同,但多参数时参数级更灵活。

在实际开发中,参数级管道比方法级管道用的多,方法级管道一般只在特殊场景下使用。

参数级管道

typescript
@Post('demo/:id')
demo(
  @Param('id', new ParseIntPipe()) id: number,
  @Query('q', new DefaultValuePipe('default')) q: string,
  @Body(new ValidationPipe()) body: MyDto,
) { ... }

优点:

  • 只对当前参数生效,类型安全,不会误伤其他参数。

  • 代码可读性强,维护简单。

  • 适合大多数业务场景(如单独校验 id、query、body)。

方法级管道

typescript
@Post('demo')
@UsePipes(new ValidationPipe())
demo(@Body() body: MyDto) { ... }

优点:

  • 适合所有参数都需要同一套校验规则的场景。

  • 适合自定义复杂管道(如多参数联合校验、日志、全量转换等)。

缺点:

  • 所有参数都会经过同一个管道,容易混淆,需要在管道内部判断参数类型。

  • 代码维护难度略高。

全局管道

typescript
app.useGlobalPipes(new ValidationPipe());

适合全局 DTO 校验、全局数据清洗。

方法级管道示例:根据参数类型选择对应 schema 校验

展开
typescript
// zod schema 用于 body 校验
const bodySchema = z.object({
  name: z.string().min(2, { message: 'Name must be at least 2 characters long' }),
  age: z.preprocess(val => Number(val), z.number().min(3, { message: 'Age must be at least 3' })),
});
// zod schema 用于 query 校验
const querySchema = z.object({
  q: z.string().min(1, { message: 'q 不能为空' }),
});
// zod schema 用于 param 校验
const paramSchema = z.object({
  id: z.preprocess(val => Number(val), z.number().int().min(1, { message: 'id 必须为正整数' })),
});
typescript
class MultiTypePipe implements PipeTransform {
  constructor(
    private paramSchema: z.ZodTypeAny,
    private querySchema: z.ZodTypeAny,
    private bodySchema: z.ZodTypeAny
  ) {}
  transform(value: any, metadata: ArgumentMetadata) {
    if (metadata.type === 'param') {
      const result = this.paramSchema.safeParse({ id: value });
      if (!result.success) {
        const msg = result.error.issues.map(i => i.message).join('; ');
        throw new BadRequestException('路径参数 id 校验失败: ' + msg);
      }
      console.log('param:', value);
      return value;
    }
    if (metadata.type === 'query') {
      const result = this.querySchema.safeParse(value);
      if (!result.success) {
        const msg = result.error.issues.map(i => i.message).join('; ');
        throw new BadRequestException('查询参数 q 校验失败: ' + msg);
      }
      console.log('query:', value);
      return value;
    }
    if (metadata.type === 'body') {
      // 兼容 body 是字符串的情况
      if (typeof value === 'string') {
        try { value = JSON.parse(value); } catch { throw new BadRequestException('Body 不是有效的 JSON'); }
      }
      const result = this.bodySchema.safeParse(value);
      if (!result.success) {
        const msg = result.error.issues.map(i => i.message).join('; ');
        throw new BadRequestException('请求体校验失败: ' + msg);
      }
      console.log('body:', value);
      return value;
    }
    return value;
  }
}
typescript
@Post('demo/:id')
@UsePipes(new MultiTypePipe(paramSchema, querySchema, bodySchema))
pipeDemoMethod(
    @Param('id') id: number,
    @Query() query: any,
    @Body() body: any,
    ) {
        return {
            param: id,
            query,
            body,
        };
    }

类验证器

警告:本节的技术依赖 TypeScript,普通 JavaScript 应用无法使用这些方法,因为 class-validator 需要 TypeScript 的元数据支持。

NestJS 与 class-validator 库无缝集成,通过基于装饰器的验证方式简化 DTO 验证。结合 class-transformer,可以实现强大的类型转换和验证功能。本节展示如何使用 class-validatorCreateCatDto 添加验证规则,并通过自定义 ValidationPipe 或内置 ValidationPipe 验证请求参数。相比 Zod 验证,class-validator 的优势在于直接在 DTO 类上定义验证规则,无需额外的模式文件,保持单一职责。

安装所需的库:

bash
npm install --save class-validator class-transformer

定义 DTO 与验证装饰器

CreateCatDto 添加 class-validator 装饰器,直接在类中定义验证规则:

typescript
import { IsString, IsInt, IsNotEmpty, Min } from 'class-validator';

export class CreateCatDto {
  @IsString()
  @IsNotEmpty()
  name: string;

  @IsInt()
  @Min(1)
  age: number;

  @IsString()
  @IsNotEmpty()
  breed: string;
}

代码解释

  • @IsString():确保 namebreed 是字符串。
  • @IsNotEmpty():确保字段非空(nullundefined 无效)。
  • @IsInt():确保 age 是整数。
  • @Min(1):确保 age 大于或等于 1。
  • 优势:验证规则与 DTO 定义合二为一,符合单一职责原则(SRP),无需单独的验证类。

提示:更多 class-validator 装饰器(如 @MaxLength@IsEmail 等)请参考 官方文档

自定义 ValidationPipe

创建一个自定义 ValidationPipe 使用 class-validatorclass-transformer 验证 DTO:

typescript
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    // 跳过基本类型(String、Number 等)
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }

    // 将普通对象转为 DTO 实例
    const object = plainToInstance(metatype, value);
    // 使用 class-validator 验证
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException('Validation failed');
    }
    return value; // 返回原始值
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype); // 仅验证复杂类型(如 DTO),跳过基本类型(如 String、Number 等)
  }
}

代码解释

  • async transform:标记为异步,因为 class-validator 的验证(如 @IsEmail)可能涉及异步操作。
  • plainToInstance:将普通 JavaScript 对象(如请求体 { name: "Fluffy" })转为 CreateCatDto 实例,以便应用装饰器验证。
  • validate:运行 class-validator 验证,失败时返回错误数组。
  • toValidate:跳过基本类型(StringNumber 等),仅验证复杂类型(如 DTO 类),当正在处理的当前参数是原生 JavaScript 类型时,它负责绕过验证步骤(它们不能附加验证装饰器,因此没有理由让它们通过验证步骤)。
  • 异常:验证失败抛出 BadRequestException,由异常过滤器处理。

提示:NestJS 提供内置的 ValidationPipe@nestjs/common),功能更强大,支持更多选项(如 whitelisttransform)。本例中的自定义管道仅为演示,生产环境推荐使用内置版本。

注意class-transformerclass-validator 由同一作者开发,协同性强,plainToInstance 确保验证前将输入转为类型化对象。

绑定 ValidationPipe

将管道绑定到控制器方法的 @Body() 参数:

typescript
import { Controller, Post, Body } from '@nestjs/common';
import { ValidationPipe } from './validation.pipe';
import { CreateCatDto } from './create-cat.dto';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Post()
  async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
    return this.catsService.create(createCatDto);
  }
}

代码解释

  • @Body(new ValidationPipe()):参数级绑定,仅验证请求体 createCatDto
  • 验证流程:请求体通过 plainToInstance 转为 CreateCatDto 实例,validate 检查装饰器规则,失败抛出异常。

增强 ValidationPipe 提供详细错误:

typescript
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToInstance(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException(
        errors
          .map(err => Object.values(err.constraints).join(', '))
          .join('; '),
      );
    }
    return value;
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

核心功能

  • class-validator:通过装饰器(如 @IsString@IsInt)定义 DTO 验证规则。
  • class-transformer:将普通对象转为 DTO 实例,支持验证和类型转换。
  • 参数级绑定@Body(new ValidationPipe()) 仅验证请求体,适合单一参数验证。
  • 异步支持transform 方法支持异步,兼容 class-validator 的异步验证。

开发建议

使用内置 ValidationPipe,生产环境推荐使用 NestJS 的内置 ValidationPipe,支持更多选项:

typescript
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useValue: new ValidationPipe({
        whitelist: true, // 过滤非 DTO 属性
        forbidNonWhitelisted: true, // 非 DTO 属性抛异常
        transform: true, // 自动类型转换
        transformOptions: { enableImplicitConversion: true }, // 隐式转换
      }),
    },
  ],
})
export class AppModule {}

全局作用域管道

ValidationPipe 设计为通用的验证工具,通过将其设置为全局作用域管道,可以应用于整个应用的每个路由处理程序,统一验证所有控制器参数(如 @Body()@Query()@Param())。以下是两种设置全局管道的方式,以及对网关、微服务和非混合微服务的解释和示例。

main.ts 中设置全局管道

通过 app.useGlobalPipes() 在应用初始化时设置全局管道:

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // 过滤非 DTO 属性
      forbidNonWhitelisted: true, // 非 DTO 属性抛异常
      transform: true, // 自动类型转换
      transformOptions: { enableImplicitConversion: true }, // 隐式转换
    }),
  );
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

代码解释

app.useGlobalPipes(new ValidationPipe()):为整个应用设置全局管道,验证所有控制器方法的参数。

配置选项

  • whitelist: true:忽略 DTO 未定义的属性。
  • forbidNonWhitelisted: true:非 DTO 属性抛出 BadRequestException
  • transform: true:自动将输入转为 DTO 定义的类型(如 "2" 转为 2)。

局限性

  • 混合应用(包含 HTTP 和微服务的应用)中,useGlobalPipes() 不适用于网关(如 WebSocket 网关)或微服务
  • 全局管道无法通过 DI(依赖注入)注入依赖,因为它在模块上下文之外注册。

注意:对于标准(非混合)微服务应用,useGlobalPipes() 仍然有效,但仅限于微服务上下文。

在模块中设置全局管道

通过 APP_PIPE 令牌在模块中注册全局管道,利用 DI 容器:

typescript
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: ValidationPipe, // 或 useValue: new ValidationPipe({...})
    },
  ],
})
export class AppModule {}

代码解释

APP_PIPE:NestJS 提供的令牌,用于注册全局管道。

useClass: ValidationPipe:让 DI 容器自动实例化管道,支持依赖注入。

优势

  • 支持 DI,管道可以注入其他服务(如配置服务)。
  • 全局作用域,适用于所有控制器和路由处理程序。

适用模块:可以在任意模块(如 AppModule)中定义,管道对整个应用生效。

提示useClass 是注册自定义提供者的常用方式,也可以用 useValue 传递实例(new ValidationPipe({...}))或 useFactory 动态创建管道。

网关、微服务和非混合微服务的解释与示例

网关(Gateway)

定义:网关是 NestJS 中处理实时通信(如 WebSocket、gRPC)的组件,通常用于实时应用(如聊天、通知)。网关通过 @WebSocketGateway() 装饰器定义,不直接处理 HTTP 请求,因此不受 app.useGlobalPipes() 影响。

示例:一个简单的 WebSocket 网关,用于实时聊天:

typescript
import { WebSocketGateway, SubscribeMessage, MessageBody } from '@nestjs/websockets';
import { IsString, IsNotEmpty } from 'class-validator';

// DTO 定义
class MessageDto {
  @IsString()
  @IsNotEmpty()
  content: string;
}

@WebSocketGateway()
export class ChatGateway {
  @SubscribeMessage('message')
  handleMessage(@MessageBody() message: MessageDto) {
    return { event: 'message', data: message.content };
  }
}

模块注册

typescript
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';

@Module({
  providers: [
    ChatGateway,
    {
      provide: APP_PIPE,
      useValue: new ValidationPipe({ transform: true }),
    },
  ],
})
export class AppModule {}

说明

@WebSocketGateway():定义 WebSocket 网关,监听客户端事件。

@SubscribeMessage('message'):处理名为 message 的事件。

管道限制app.useGlobalPipes() 不适用于网关,但 APP_PIPE 在模块中注册的管道对网关生效。

测试

  • 客户端发送 { "content": "Hello" }:验证通过,返回 { event: "message", data: "Hello" }
  • 客户端发送 { "content": "" }:抛出 BadRequestException

微服务(Microservice)

定义:微服务是 NestJS 中用于分布式系统的模块,通过 @nestjs/microservices 提供支持(如 TCP、Redis、gRPC 通信)。微服务不依赖 HTTP 协议,通常用于模块化、高可扩展的架构。微服务可以是混合应用(与 HTTP 应用共存)或非混合微服务(独立运行)。

示例:一个 TCP 微服务,处理猫咪创建请求:

typescript
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { IsString, IsInt, IsNotEmpty, Min } from 'class-validator';

// DTO 定义
class CreateCatDto {
  @IsString()
  @IsNotEmpty()
  name: string;

  @IsInt()
  @Min(1)
  age: number;

  @IsString()
  @IsNotEmpty()
  breed: string;
}

@Controller()
export class CatsMicroserviceController {
  @MessagePattern({ cmd: 'create_cat' })
  createCat(data: CreateCatDto) {
    return { message: 'Cat created', data };
  }
}

模块注册

typescript
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { CatsMicroserviceController } from './cats-microservice.controller';

@Module({
  controllers: [CatsMicroserviceController],
  providers: [
    {
      provide: APP_PIPE,
      useValue: new ValidationPipe({ transform: true }),
    },
  ],
})
export class AppModule {}

启动微服务

typescript
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
    transport: Transport.TCP,
    options: { host: '127.0.0.1', port: 3001 },
  });
  await app.listen();
}
bootstrap();

说明

@MessagePattern({ cmd: 'create_cat' }):监听 create_cat 消息。

管道支持APP_PIPE 注册的全局管道对微服务有效,验证 CreateCatDto

测试

  • 客户端发送 { cmd: "create_cat", data: { name: "Fluffy", age: 2, breed: "Persian" } }:返回 { message: "Cat created", data: {...} }
  • 客户端发送 { cmd: "create_cat", data: { name: "", age: -1 } }:抛出 BadRequestException

非混合微服务

定义:非混合微服务是指独立运行的微服务应用,不与 HTTP 服务器(如 Express)结合。app.useGlobalPipes() 在这种场景下仍然有效,因为它只处理微服务上下文。

示例:独立 TCP 微服务(与上述类似,但明确为非混合):

typescript
// cats-microservice.controller.ts (同上)
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { IsString, IsInt, IsNotEmpty, Min } from 'class-validator';

class CreateCatDto {
  @IsString()
  @IsNotEmpty()
  name: string;

  @IsInt()
  @Min(1)
  age: number;

  @IsString()
  @IsNotEmpty()
  breed: string;
}

@Controller()
export class CatsMicroserviceController {
  @MessagePattern({ cmd: 'create_cat' })
  createCat(data: CreateCatDto) {
    return { message: 'Cat created', data };
  }
}

模块

typescript
import { Module } from '@nestjs/common';
import { CatsMicroserviceController } from './cats-microservice.controller';

@Module({
  controllers: [CatsMicroserviceController],
})
export class AppModule {}

启动

typescript
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
    transport: Transport.TCP,
    options: { host: '127.0.0.1', port: 3001 },
  });
  app.useGlobalPipes(new ValidationPipe({ transform: true }));
  await app.listen();
}
bootstrap();

说明

  • 非混合:此应用仅作为微服务运行,不包含 HTTP 服务器。
  • app.useGlobalPipes():在非混合微服务中有效,验证所有消息模式的数据。
  • 测试:同上,验证 CreateCatDto 的字段。

混合应用

定义:混合应用同时支持 HTTP 和微服务(如 WebSocket、TCP),通过 NestFactory.create 创建 HTTP 应用,再附加微服务。

示例

typescript
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule); // HTTP 应用
  app.useGlobalPipes(new ValidationPipe({ transform: true }));

  // 附加微服务
  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.TCP,
    options: { host: '127.0.0.1', port: 3001 },
  });

  await app.startAllMicroservices(); // 启动微服务
  await app.listen(3000); // 启动 HTTP 服务
}
bootstrap();

说明

  • app.useGlobalPipes():仅对 HTTP 路由生效,不影响微服务。
  • 解决办法:在 AppModule 中使用 APP_PIPE 注册全局管道,确保 HTTP 和微服务都受影响。

内置的 ValidationPipe

NestJS 提供了一个内置的 ValidationPipe(从 @nestjs/common 导入),无需自行构建即可实现强大的 DTO 验证功能。相比我们在前几节中创建的自定义验证管道,内置 ValidationPipe 提供了更多配置选项(如 whitelisttransform 等),功能更全面,适合生产环境。本节将简要介绍内置 ValidationPipe 的用法和优势,并通过示例展示如何结合 class-validatorclass-transformer 验证 DTO。

提示:内置 ValidationPipe 的完整文档和更多示例可参考 验证章节

核心功能与配置选项

内置 ValidationPipe 的主要功能是通过 class-validatorclass-transformer 验证和转换请求参数(如 @Body()@Query()@Param())。它支持以下关键配置选项:

  • whitelist: true:过滤掉 DTO 中未定义的属性,防止意外数据进入。
  • forbidNonWhitelisted: true:非 DTO 定义的属性抛出 BadRequestException
  • transform: true:自动将输入转为 DTO 定义的类型(如字符串 "2" 转为整数 2)。
  • transformOptions: { enableImplicitConversion: true }:启用隐式类型转换(通过类型),无需显式装饰器。
  • disableErrorMessages: boolean:禁用详细错误消息(安全性考虑)。
  • exceptionFactory:自定义异常生成逻辑。

代码示例

  1. 安装依赖

确保安装 class-validatorclass-transformer

bash
npm install --save class-validator class-transformer

tsconfig.json 中启用元数据支持:

json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strictNullChecks": true
  }
}
  1. 定义 DTO

使用 class-validator 装饰器定义验证规则:

typescript
import { IsString, IsInt, IsNotEmpty, Min, MinLength } from 'class-validator';

export class CreateCatDto {
  @IsString()
  @IsNotEmpty()
  @MinLength(2)
  name: string;

  @IsInt()
  @Min(1)
  age: number;

  @IsString()
  @IsNotEmpty()
  breed: string;
}

说明

  • @IsString()@IsNotEmpty():确保 namebreed 是非空字符串。
  • @MinLength(2):确保 name 至少 2 个字符。
  • @IsInt()@Min(1):确保 age 是正整数。
  1. 使用内置 ValidationPipe

参数级绑定

在控制器方法中为 @Body() 绑定 ValidationPipe

typescript
import { Controller, Post, Body } from '@nestjs/common';
import { ValidationPipe } from '@nestjs/common';
import { CreateCatDto } from './create-cat.dto';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Post()
  async create(@Body(new ValidationPipe({ transform: true })) createCatDto: CreateCatDto) {
    return this.catsService.create(createCatDto);
  }
}

代码解释

  • @Body(new ValidationPipe({ transform: true })):仅验证请求体,自动将输入转为 CreateCatDto 类型。
  • 验证行为:检查 nameagebreed 是否符合规则,失败抛出 BadRequestException

全局管道

main.ts 或模块中设置全局管道:

方式 1:在 main.ts

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

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

方式 2:在 AppModule

typescript
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [
    CatsService,
    {
      provide: APP_PIPE,
      useValue: new ValidationPipe({
        whitelist: true,
        forbidNonWhitelisted: true,
        transform: true,
        transformOptions: { enableImplicitConversion: true },
      }),
    },
  ],
})
export class AppModule {}

说明

  • APP_PIPE:支持依赖注入,适用于 HTTP、网关和微服务。
  • 全局效果:验证所有控制器参数(@Body()@Query()@Param())。

开发建议

  1. 优化全局管道配置
typescript
@Module({
  providers: [
    {
      provide: APP_PIPE,
      useValue: new ValidationPipe({
        whitelist: true,
        forbidNonWhitelisted: true,
        transform: true,
        transformOptions: { enableImplicitConversion: true },
        exceptionFactory: (errors) =>
          new BadRequestException(
            errors
              .map((err) => Object.values(err.constraints).join(', '))
              .join('; '),
          ),
      }),
    },
  ],
})
export class AppModule {}
  • 自定义异常:通过 exceptionFactory 提供详细错误消息。
  1. 结合其他管道

为特定参数使用其他管道(如 ParseIntPipe):

typescript
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}
  • 全局管道:验证 DTO(如 @Body())。
  • 参数级管道:处理特定参数(如 id)。

转换用例

管道(Pipes)不仅用于验证,还能通过 transform 方法将输入数据转换为所需格式。转换管道的返回值会完全覆盖原始参数值,这使其非常适合处理客户端输入的预处理,例如将字符串转为整数、应用默认值,或从数据库获取实体。本节展示如何实现一个简单的自定义 ParseIntPipe,以及一个从 ID 获取用户实体的 UserByIdPipe,并解释转换管道的典型用例和绑定方式。

提示:NestJS 提供内置的 ParseIntPipe,功能更强大(支持配置错误状态码等)。本例中的自定义 ParseIntPipe 仅为演示目的,保持简单。

核心功能

转换:将输入(如字符串)转为目标格式(如整数、对象)。

覆盖值transform 方法的返回值替换原始参数值。

用例:

  • 将字符串转为数字(如 "123"123)。
  • 为缺失字段设置默认值。
  • 从数据库查询实体(如 ID → UserEntity)。

代码示例

  1. 自定义 ParseIntPipe

实现一个简单的 ParseIntPipe,将字符串转为整数:

typescript
import { PipeTransform, Injectable, BadRequestException, ArgumentMetadata } 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('Validation failed: Expected a numeric string');
    }
    return val; // 返回转换后的整数
  }
}

代码解释

  • PipeTransform<string, number>:泛型接口,指定输入为 string,输出为 number
  • parseInt(value, 10):将字符串转为十进制整数。
  • isNaN(val):检查转换是否失败,失败抛出 BadRequestException
  • 返回值:转换后的整数覆盖原始参数值。
  1. 绑定 ParseIntPipe

ParseIntPipe 绑定到路由参数:

typescript
import { Controller, Get, Param } from '@nestjs/common';
import { ParseIntPipe } from './parse-int.pipe';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get(':id')
  async findOne(@Param('id', new ParseIntPipe()) id: number) {
    return this.catsService.findOne(id); // id 已转为整数
  }
}

行为

  • 请求 GET /cats/123id 转为 123(number),调用 catsService.findOne(123)
  • 请求 GET /cats/abc:抛出 BadRequestException,错误消息为 "Validation failed: Expected a numeric string"
  1. 自定义 UserByIdPipe

实现一个从 ID 获取用户实体的转换管道:

typescript
import { PipeTransform, Injectable, BadRequestException, ArgumentMetadata } from '@nestjs/common';
import { UsersService } from './users.service';

@Injectable()
export class UserByIdPipe implements PipeTransform<string, Promise<UserEntity>> {
  constructor(private readonly usersService: UsersService) {}

  async transform(value: string, metadata: ArgumentMetadata): Promise<UserEntity> {
    const id = parseInt(value, 10);
    if (isNaN(id)) {
      throw new BadRequestException('Invalid ID: Expected a numeric string');
    }
    const user = await this.usersService.findById(id);
    if (!user) {
      throw new BadRequestException(`User with ID ${id} not found`);
    }
    return user; // 返回 UserEntity
  }
}

interface UserEntity {
  id: number;
  name: string;
}

代码解释

  • 依赖注入:注入 UsersService 用于数据库查询。
  • transform:异步方法,将字符串 ID 转为整数,查询数据库,返回 UserEntity
  • 异常:ID 无效或用户不存在时抛出 BadRequestException
  1. 绑定 UserByIdPipe

UserByIdPipe 绑定到路由参数:

typescript
import { Controller, Get, Param } from '@nestjs/common';
import { UserByIdPipe } from './user-by-id.pipe';
import { UserEntity } from './users.service';

@Controller('users')
export class UsersController {
  @Get(':id')
  findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
    return userEntity; // 直接返回用户实体
  }
}

模拟服务

typescript
import { Injectable } from '@nestjs/common';
import { UserEntity } from './users.service';

@Injectable()
export class UsersService {
  async findById(id: number): Promise<UserEntity | null> {
    // 模拟数据库查询
    const users: UserEntity[] = [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ];
    return users.find((user) => user.id === id) || null;
  }
}

行为

  • 请求 GET /users/1:返回 { id: 1, name: "Alice" }
  • 请求 GET /users/abc:抛出 BadRequestException: Invalid ID: Expected a numeric string
  • 请求 GET /users/3:抛出 BadRequestException: User with ID 3 not found
  1. 全局异常过滤器

确保管道抛出的异常被格式化:

typescript
import { Catch, ArgumentsHost, HttpStatus, HttpException } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
    const responseBody = {
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: this.httpAdapter.getRequestUrl(ctx.getRequest()),
      message: exception.message || 'Something went wrong',
    };
    this.httpAdapter.reply(ctx.getResponse(), responseBody, status);
  }
}

注册

typescript
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
    controllers: [UsersController],
    providers: [UsersService, { provide: APP_FILTER, useClass: AllExceptionsFilter }],
})
export class AppModule {}

开发建议

  1. 使用内置 ParseIntPipe

生产环境推荐 NestJS 内置的 ParseIntPipe

typescript
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}
  • 支持配置(如 new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_FOUND }))。
  1. 实现默认值管道

为缺失字段添加默认值:

typescript
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class DefaultValuePipe implements PipeTransform {
  constructor(private defaultValue: any) {}

  transform(value: any, metadata: ArgumentMetadata) {
    return value ?? this.defaultValue; // 如果值为空,返回默认值
  }
}

使用

typescript
@Get()
async findAll(@Query('page', new DefaultValuePipe(1)) page: number) {
  return this.catsService.findAll(page);
}
  1. 优化 UserByIdPipe

缓存查询结果以提高性能:

typescript
@Injectable()
export class UserByIdPipe implements PipeTransform<string, Promise<UserEntity>> {
  private cache = new Map<number, UserEntity>();

  constructor(private readonly usersService: UsersService) {}

  async transform(value: string, metadata: ArgumentMetadata): Promise<UserEntity> {
    const id = parseInt(value, 10);
    if (isNaN(id)) {
      throw new BadRequestException('Invalid ID: Expected a numeric string');
    }
    if (this.cache.has(id)) {
      return this.cache.get(id)!;
    }
    const user = await this.usersService.findById(id);
    if (!user) {
      throw new BadRequestException(`User with ID ${id} not found`);
    }
    this.cache.set(id, user);
    return user;
  }
}

提供默认值

Parse* 管道(如 ParseIntPipeParseBoolPipe)要求输入参数值必须存在,若收到 nullundefined,会抛出异常。为了处理缺失的查询字符串参数(如可选的 pageactiveOnly),可以使用 DefaultValuePipeParse* 管道之前为缺失值注入默认值。

核心功能

  • DefaultValuePipe:为缺失或 undefined 的参数提供默认值。
  • Parse\* 管道结合:先注入默认值,再由 Parse* 管道进行类型转换。
  • 适用场景:处理可选查询参数(如分页参数 page 或过滤条件 activeOnly)。

代码示例

  1. 使用 DefaultValuePipe 和 Parse* 管道

在控制器方法中为查询参数绑定 DefaultValuePipeParse* 管道:

typescript
import { Controller, Get, Query } from '@nestjs/common';
import { DefaultValuePipe, ParseBoolPipe, ParseIntPipe } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get()
  async findAll(
    @Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
    @Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
  ) {
    return this.catsService.findAll({ activeOnly, page });
  }
}

代码解释

DefaultValuePipe(false):若 activeOnly 缺失或 undefined,提供默认值 false

ParseBoolPipe:将字符串(如 "true""false")转为布尔值,失败抛出 BadRequestException

DefaultValuePipe(0):若 page 缺失或 undefined,提供默认值 0

ParseIntPipe:将字符串转为整数,失败抛出 BadRequestException

管道顺序:管道按声明顺序执行,DefaultValuePipe 先处理缺失值,Parse* 管道再进行转换。

最后更新时间: