NestJS 管道
目录
管道
管道(Pipes)是用 @Injectable()
装饰器标注的类,实现了 PipeTransform
接口,就像流水线上的“加工站”,对输入数据进行处理或校验。管道有两个典型用途:
- 转型(Transformation):将输入数据转换为所需格式,比如把字符串 “123” 变成整数 123。
- 验证(Validation):检查输入数据是否合法,合格的直接放行,不合格的抛出异常。
管道作用于控制器路由处理器(即控制器方法)接收的参数,在方法执行前运行。NestJS 会先将参数交给管道处理,管道对参数进行转换或验证,处理后的结果(或抛出的异常)再传递给路由处理器。
NestJS 内置了许多开箱即用的管道,你也可以打造自己的自定义管道。
提示:管道运行在“异常区域”(exception zone)内。如果管道抛出异常,会由异常层(全局异常过滤器或当前上下文的异常过滤器)处理。因此,管道抛出异常后,控制器方法不会被执行。这为你在系统边界验证外部输入数据提供了一种最佳实践,就像在“入口关卡”过滤掉不合格的数据。
以下是管道的核心接口 PipeTransform
的定义,带注释说明其作用:
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
,用于将字符串转换为整数并验证:
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*
管道,包括 ParseBoolPipe
、ParseFloatPipe
、ParseEnumPipe
、ParseArrayPipe
、ParseDatePipe
和 ParseUUIDPipe
。
以下是 ParseIntPipe
的使用示例,展示如何将查询参数转换为整数:
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
的简化实现,展示其工作原理:
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
)应用管道:
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 已转为整数
}
}
这种绑定方式确保以下两者之一成立:
- 控制器方法(如
findOne
)接收到的参数是预期类型(例如整数id
),符合catsService.findOne
的要求。 - 如果参数无效(例如非数字字符串),管道抛出异常,阻止方法执行。
示例场景: 假设请求为 GET localhost:3000/abc
,ParseIntPipe
会抛出以下异常:
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
这个异常会阻止 findOne
方法执行,确保只有有效数据进入业务逻辑。
在上面的代码中,我们传递了管道类(ParseIntPipe
)而非实例,将实例化责任交给 NestJS 框架,启用依赖注入(DI)。管道和守卫类似,也可以传递就地实例,以便自定义行为。例如,修改 ParseIntPipe
的错误状态码:
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
这种绑定方式同样适用于其他 Parse*
管道(如 ParseBoolPipe
、ParseUUIDPipe
等),它们都能处理路由参数、查询参数或请求体。以下是查询参数的例子:
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
再看一个使用 ParseUUIDPipe
的例子,确保参数是有效的 UUID:
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。如果需要特定版本,可以通过选项指定:
@Param('uuid', new ParseUUIDPipe({ version: '4' }))
uuid: string;
绑定 Parse*
管道的方式类似,主要用于转型和验证。ValidationPipe
的绑定稍有不同。
提示:更多 ValidationPipe
的详细示例,请参阅 验证技术。
定制管道
除了使用 NestJS 提供的内置管道(如 ParseIntPipe
和 ValidationPipe
),你还可以从头打造自定义管道。虽然内置管道已经很强大,但通过构建自定义版本,我们可以深入理解管道的实现原理。本节将从一个简单的 ValidationPipe
开始,逐步展示如何创建自定义管道。
我们先构建一个最简单的 ValidationPipe
,它像一个“中转站”,接收输入值并直接返回,不做任何处理,类似数学中的恒等函数(输入什么,输出什么):
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
)包含以下属性:
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 类型(如String
、Number
)。如果未声明类型或使用普通 JavaScript,值为undefined
。data
:装饰器中传递的字符串,例如@Body('key')
的key
。如果装饰器括号为空(如@Body()
),则为undefined
。
警告:TypeScript 的接口(interface)在编译为 JavaScript 后会被完全移除,运行时无法获取接口的任何类型信息。因此,如果你的方法参数使用接口类型,NestJS 在运行时获取到的 metatype
只会是 Object
,无法精确反映原本的类型结构。
如何解决? 如果你需要在运行时进行类型检查、验证或元数据反射(如在自定义管道或 ValidationPipe
中),建议使用 类(class) 来定义 DTO(数据传输对象),而不是接口。因为类在编译后依然存在于 JavaScript 代码中,NestJS 可以通过反射机制获取到类的构造函数和类型信息。
基于模式的验证
让我们的验证管道更有用一些。仔细查看 CatsController
的 create()
方法,我们可能希望在尝试运行我们的服务方法之前确保请求体对象有效。
接下来,我们关注基于模式的验证。以 CatsController
的 create
方法为例:
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto); // createCatDto 需验证
}
CreateCatDto
定义如下:
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
我们需要确保 createCatDto
的请求体有效(例如,name
和 breed
是非空字符串,age
是正整数)。直接在控制器方法中验证会违反单一职责原则(SRP),增加代码复杂性。另一种方法是创建一个验证器类,但需要在每个方法开头手动调用,容易遗漏。
中间件可能看似是个选择,但它无法访问执行上下文(如控制器方法或参数),因此不适合通用验证。而管道正是为此设计的:它们在方法调用前处理参数,完美适合验证 createCatDto
。
对象模式验证
对象模式验证(Schema-Based Validation)是一种以干净、DRY(Don't Repeat Yourself)方式验证复杂对象(如 DTO)的方法。相比在控制器中手动验证,使用基于模式的验证可以让代码更简洁、可复用。Zod 是一个强大的模式验证库,API 直观,适合与 NestJS 的管道机制结合。本节将展示如何使用 Zod 创建一个自定义验证管道,专注于简单性和可重用性。
DRY(Don't Repeat Yourself) 是软件开发中的一个重要原则,意思是:不要重复自己,即相同的逻辑只写一次,重复的代码抽出来。
首先,安装 Zod 库:
npm install --save zod
以下代码展示了一个基于 Zod 的验证管道 ZodValidationPipe
,它通过构造函数接收一个 Zod 模式(ZodSchema
),并使用 schema.parse()
方法验证输入参数。验证管道要么返回通过验证的值,要么抛出异常。
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
:
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
绑定管道到控制器方法:
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 验证
}
}
测试:
POST /cats { "name": "Fluffy", "age": 2, "breed": "Persian" }
验证通过,返回 { cat: { name: "Fluffy", age: 2, breed: "Persian" } }
。
POST /cats { "name": "F", "age": -1, "breed": "" }
抛出 BadRequestException
,错误消息为 "Validation failed"
。
POST /cats { "name": "Fluffy", "age": "2", "breed": "Persian" }
Zod 自动将 "2"
转为 2
(z.number()
的行为)。
核心功能
- ZodValidationPipe:通过 Zod 模式验证输入数据,确保符合定义的规则(如类型、长度、范围)。
- 可重用性:通过构造函数传递不同 Zod 模式,管道可用于多种 DTO 验证。
- 异常处理:验证失败抛出
BadRequestException
,由异常过滤器捕获,保护控制器方法。
绑定验证管道
我们之前学习了如何绑定转换管道(如 ParseIntPipe
和其他 Parse*
管道),它们专注于将输入转为特定类型(如字符串到整数)。绑定验证管道(如 ZodValidationPipe
)同样简单,但通常在方法级别而非参数级别应用,适合验证复杂的对象(如请求体的 DTO)。将 ZodValidationPipe
绑定到控制器方法,确保 CreateCatDto
在进入业务逻辑前通过 Zod 模式验证。
绑定 ZodValidationPipe
的步骤:
- 创建
ZodValidationPipe
的实例。 - 在管道构造函数中传递上下文特定的 Zod 模式(schema)。
- 使用
@UsePipes()
装饰器将管道绑定到方法。
Zod 模式示例
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()
:确保字段非空(null
或undefined
会被拒绝)。z.infer
:从模式推导 TypeScript 类型,确保类型安全。
绑定管道
使用 @UsePipes()
装饰器将 ZodValidationPipe
绑定到控制器方法:
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
以确保严格的类型检查:
{
"compilerOptions": {
"strictNullChecks": true
}
}
增强 ZodValidationPipe
提供详细错误:
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 模式,添加更严格的验证规则:
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()
在方法级别绑定管道
@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())
在参数级别绑定管道
@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()
场景下,两者效果相同,但多参数时参数级更灵活。
在实际开发中,参数级管道比方法级管道用的多,方法级管道一般只在特殊场景下使用。
参数级管道
@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)。
方法级管道
@Post('demo')
@UsePipes(new ValidationPipe())
demo(@Body() body: MyDto) { ... }
优点:
适合所有参数都需要同一套校验规则的场景。
适合自定义复杂管道(如多参数联合校验、日志、全量转换等)。
缺点:
所有参数都会经过同一个管道,容易混淆,需要在管道内部判断参数类型。
代码维护难度略高。
全局管道
app.useGlobalPipes(new ValidationPipe());
适合全局 DTO 校验、全局数据清洗。
方法级管道示例:根据参数类型选择对应 schema 校验
展开
// 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 必须为正整数' })),
});
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;
}
}
@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-validator
为 CreateCatDto
添加验证规则,并通过自定义 ValidationPipe
或内置 ValidationPipe
验证请求参数。相比 Zod 验证,class-validator
的优势在于直接在 DTO 类上定义验证规则,无需额外的模式文件,保持单一职责。
安装所需的库:
npm install --save class-validator class-transformer
定义 DTO 与验证装饰器
为 CreateCatDto
添加 class-validator
装饰器,直接在类中定义验证规则:
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()
:确保name
和breed
是字符串。@IsNotEmpty()
:确保字段非空(null
或undefined
无效)。@IsInt()
:确保age
是整数。@Min(1)
:确保age
大于或等于 1。- 优势:验证规则与 DTO 定义合二为一,符合单一职责原则(SRP),无需单独的验证类。
提示:更多 class-validator
装饰器(如 @MaxLength
、@IsEmail
等)请参考 官方文档。
自定义 ValidationPipe
创建一个自定义 ValidationPipe
使用 class-validator
和 class-transformer
验证 DTO:
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
:跳过基本类型(String
、Number
等),仅验证复杂类型(如 DTO 类),当正在处理的当前参数是原生 JavaScript 类型时,它负责绕过验证步骤(它们不能附加验证装饰器,因此没有理由让它们通过验证步骤)。- 异常:验证失败抛出
BadRequestException
,由异常过滤器处理。
提示:NestJS 提供内置的 ValidationPipe
(@nestjs/common
),功能更强大,支持更多选项(如 whitelist
、transform
)。本例中的自定义管道仅为演示,生产环境推荐使用内置版本。
注意:class-transformer
和 class-validator
由同一作者开发,协同性强,plainToInstance
确保验证前将输入转为类型化对象。
绑定 ValidationPipe
将管道绑定到控制器方法的 @Body()
参数:
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
提供详细错误:
@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
,支持更多选项:
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()
在应用初始化时设置全局管道:
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 容器:
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 网关,用于实时聊天:
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 };
}
}
模块注册:
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 微服务,处理猫咪创建请求:
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 };
}
}
模块注册:
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 {}
启动微服务:
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 微服务(与上述类似,但明确为非混合):
// 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 };
}
}
模块:
import { Module } from '@nestjs/common';
import { CatsMicroserviceController } from './cats-microservice.controller';
@Module({
controllers: [CatsMicroserviceController],
})
export class AppModule {}
启动:
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 应用,再附加微服务。
示例:
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
提供了更多配置选项(如 whitelist
、transform
等),功能更全面,适合生产环境。本节将简要介绍内置 ValidationPipe
的用法和优势,并通过示例展示如何结合 class-validator
和 class-transformer
验证 DTO。
提示:内置 ValidationPipe
的完整文档和更多示例可参考 验证章节
核心功能与配置选项
内置 ValidationPipe
的主要功能是通过 class-validator
和 class-transformer
验证和转换请求参数(如 @Body()
、@Query()
、@Param()
)。它支持以下关键配置选项:
whitelist: true
:过滤掉 DTO 中未定义的属性,防止意外数据进入。forbidNonWhitelisted: true
:非 DTO 定义的属性抛出BadRequestException
。transform: true
:自动将输入转为 DTO 定义的类型(如字符串"2"
转为整数2
)。transformOptions: { enableImplicitConversion: true }
:启用隐式类型转换(通过类型),无需显式装饰器。disableErrorMessages: boolean
:禁用详细错误消息(安全性考虑)。exceptionFactory
:自定义异常生成逻辑。
代码示例
- 安装依赖
确保安装 class-validator
和 class-transformer
:
npm install --save class-validator class-transformer
在 tsconfig.json
中启用元数据支持:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictNullChecks": true
}
}
- 定义 DTO
使用 class-validator
装饰器定义验证规则:
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()
:确保name
和breed
是非空字符串。@MinLength(2)
:确保name
至少 2 个字符。@IsInt()
和@Min(1)
:确保age
是正整数。
- 使用内置 ValidationPipe
参数级绑定
在控制器方法中为 @Body()
绑定 ValidationPipe
:
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
类型。- 验证行为:检查
name
、age
、breed
是否符合规则,失败抛出BadRequestException
。
全局管道
在 main.ts
或模块中设置全局管道:
方式 1:在 main.ts
中:
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
中:
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()
)。
开发建议
- 优化全局管道配置
@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
提供详细错误消息。
- 结合其他管道
为特定参数使用其他管道(如 ParseIntPipe
):
@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
)。
代码示例
- 自定义 ParseIntPipe
实现一个简单的 ParseIntPipe
,将字符串转为整数:
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
。- 返回值:转换后的整数覆盖原始参数值。
- 绑定 ParseIntPipe
将 ParseIntPipe
绑定到路由参数:
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/123
:id
转为123
(number),调用catsService.findOne(123)
。 - 请求
GET /cats/abc
:抛出BadRequestException
,错误消息为"Validation failed: Expected a numeric string"
。
- 自定义 UserByIdPipe
实现一个从 ID 获取用户实体的转换管道:
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
。
- 绑定 UserByIdPipe
将 UserByIdPipe
绑定到路由参数:
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; // 直接返回用户实体
}
}
模拟服务:
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
。
- 全局异常过滤器
确保管道抛出的异常被格式化:
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);
}
}
注册:
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 {}
开发建议
- 使用内置 ParseIntPipe
生产环境推荐 NestJS 内置的 ParseIntPipe
:
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
- 支持配置(如
new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_FOUND })
)。
- 实现默认值管道
为缺失字段添加默认值:
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; // 如果值为空,返回默认值
}
}
使用:
@Get()
async findAll(@Query('page', new DefaultValuePipe(1)) page: number) {
return this.catsService.findAll(page);
}
- 优化 UserByIdPipe
缓存查询结果以提高性能:
@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*
管道(如 ParseIntPipe
、ParseBoolPipe
)要求输入参数值必须存在,若收到 null
或 undefined
,会抛出异常。为了处理缺失的查询字符串参数(如可选的 page
或 activeOnly
),可以使用 DefaultValuePipe
在 Parse*
管道之前为缺失值注入默认值。
核心功能
DefaultValuePipe
:为缺失或undefined
的参数提供默认值。- 与
Parse\*
管道结合:先注入默认值,再由Parse*
管道进行类型转换。 - 适用场景:处理可选查询参数(如分页参数
page
或过滤条件activeOnly
)。
代码示例
- 使用 DefaultValuePipe 和 Parse* 管道
在控制器方法中为查询参数绑定 DefaultValuePipe
和 Parse*
管道:
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*
管道再进行转换。