NestJS管道 (Pipes)
学习目标
- 理解管道在NestJS中的作用和地位
- 掌握内置管道的使用方法
- 学会创建和使用自定义管道
- 理解数据验证和类型转换的实现原理
- 能够应用管道确保输入数据的合法性
核心知识点
1. 管道概念
管道是NestJS中用于数据处理的组件,它们可以:
- 转换数据:将输入数据转换为所需的格式
- 验证数据:确保输入数据符合特定的规则
- 处理错误:当数据无效时抛出异常
管道在请求处理流程中的位置:
- 客户端发送请求
- 中间件处理请求
- 守卫验证权限
- 管道处理数据
- 控制器处理请求
- 服务执行业务逻辑
- 拦截器处理响应
- 异常过滤器处理异常
- 响应返回给客户端
2. 内置管道
NestJS提供了几个内置管道:
- ValidationPipe:用于数据验证
- ParseIntPipe:将字符串转换为整数
- ParseBoolPipe:将字符串转换为布尔值
- ParseArrayPipe:将字符串转换为数组
- ParseUUIDPipe:验证并转换UUID格式
- DefaultValuePipe:为缺失的参数提供默认值
3. 基本使用
路由参数验证
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return `User with id ${id}`;
}
}查询参数验证
import { Controller, Get, Query, ParseBoolPipe, DefaultValuePipe } from '@nestjs/common';
@Controller('items')
export class ItemsController {
@Get()
findAll(
@Query('active', new DefaultValuePipe(false), ParseBoolPipe) active: boolean,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
) {
return { active, page };
}
}4. ValidationPipe
ValidationPipe是最常用的管道之一,用于验证请求数据。它通常与DTO(数据传输对象)一起使用。
安装依赖
npm install class-validator class-transformer创建DTO
// create-cat.dto.ts
import { IsString, IsInt, MinLength, Max } from 'class-validator';
export class CreateCatDto {
@IsString()
@MinLength(2)
name: string;
@IsInt()
@Max(20)
age: number;
@IsString()
breed: string;
}使用ValidationPipe
import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateCatDto } from './create-cat.dto';
@Controller('cats')
export class CatsController {
@Post()
@UsePipes(new ValidationPipe())
create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
}5. 全局管道
在应用程序级别注册全局管道:
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 注册全局ValidationPipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 自动删除未在DTO中定义的属性
forbidNonWhitelisted: true, // 当存在未在DTO中定义的属性时抛出错误
transform: true, // 自动将请求数据转换为DTO类型
}));
await app.listen(3000);
}
bootstrap();6. 创建自定义管道
创建自定义管道需要实现PipeTransform接口:
// parse-date.pipe.ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseDatePipe implements PipeTransform {
transform(value: any) {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new BadRequestException('Invalid date format');
}
return date;
}
}7. 带参数的自定义管道
创建接受配置参数的自定义管道:
// parse-date.pipe.ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseDatePipe implements PipeTransform {
constructor(private readonly options?: {
nullable?: boolean;
}) {}
transform(value: any) {
if (this.options?.nullable && value === null) {
return null;
}
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new BadRequestException('Invalid date format');
}
return date;
}
}8. 使用自定义管道
import { Controller, Get, Query, UsePipes } from '@nestjs/common';
import { ParseDatePipe } from './parse-date.pipe';
@Controller('events')
export class EventsController {
@Get()
findAll(
@Query('startDate', new ParseDatePipe({ nullable: true })) startDate: Date,
@Query('endDate', ParseDatePipe) endDate: Date,
) {
return { startDate, endDate };
}
}9. 管道的依赖注入
管道可以通过依赖注入系统注入其他服务:
// validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { LoggerService } from './logger.service';
@Injectable()
export class CustomValidationPipe implements PipeTransform<any> {
constructor(private loggerService: LoggerService) {}
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
this.loggerService.error('Validation failed', JSON.stringify(errors));
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}10. 管道与异常过滤器
管道抛出的异常会被异常过滤器捕获,因此可以结合使用:
- 管道负责验证数据并抛出异常
- 异常过滤器负责统一处理异常并返回响应
实践案例分析
案例:用户注册数据验证
需求分析
我们需要创建一个用户注册API,要求:
- 验证用户名:必须是字符串,长度在3-20个字符之间
- 验证邮箱:必须是有效的邮箱格式
- 验证密码:必须至少8个字符,包含至少一个大写字母、一个小写字母和一个数字
- 验证年龄:必须是数字,在18-100之间
- 提供友好的错误信息
实现步骤
- 创建用户注册DTO
- 配置ValidationPipe
- 创建控制器处理注册请求
- 测试验证功能
代码实现
1. 创建用户注册DTO
// create-user.dto.ts
import { IsString, IsEmail, IsStrongPassword, IsInt, Min, Max, Length } from 'class-validator';
export class CreateUserDto {
@IsString()
@Length(3, 20, {
message: 'Username must be between 3 and 20 characters long',
})
username: string;
@IsEmail({}, {
message: 'Please provide a valid email address',
})
email: string;
@IsStrongPassword(
{
minLength: 8,
minUppercase: 1,
minLowercase: 1,
minNumbers: 1,
},
{
message: 'Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number',
}
)
password: string;
@IsInt({
message: 'Age must be a number',
})
@Min(18, {
message: 'You must be at least 18 years old',
})
@Max(100, {
message: 'Age must be less than or equal to 100',
})
age: number;
}2. 配置全局ValidationPipe
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 配置全局ValidationPipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
disableErrorMessages: false, // 显示错误信息
}));
await app.listen(3000);
}
bootstrap();3. 创建用户控制器
// users.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { CreateUserDto } from './create-user.dto';
@Controller('users')
export class UsersController {
@Post('register')
register(@Body() createUserDto: CreateUserDto) {
// 在实际应用中,这里会调用服务层创建用户
// 为了演示,我们只返回验证通过的数据
return {
message: 'User registered successfully',
user: {
username: createUserDto.username,
email: createUserDto.email,
age: createUserDto.age,
},
};
}
}4. 创建模块
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
@Module({
controllers: [UsersController],
})
export class UsersModule {}// app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
@Module({
imports: [UsersModule],
})
export class AppModule {}测试结果
1. 有效数据
请求:
POST /users/register
Content-Type: application/json
{
"username": "john_doe",
"email": "john@example.com",
"password": "Password123",
"age": 25
}响应:
{
"message": "User registered successfully",
"user": {
"username": "john_doe",
"email": "john@example.com",
"age": 25
}
}2. 无效数据 - 用户名太短
请求:
POST /users/register
Content-Type: application/json
{
"username": "jo",
"email": "john@example.com",
"password": "Password123",
"age": 25
}响应:
{
"statusCode": 400,
"message": ["Username must be between 3 and 20 characters long"],
"error": "Bad Request"
}3. 无效数据 - 邮箱格式错误
请求:
POST /users/register
Content-Type: application/json
{
"username": "john_doe",
"email": "invalid-email",
"password": "Password123",
"age": 25
}响应:
{
"statusCode": 400,
"message": ["Please provide a valid email address"],
"error": "Bad Request"
}4. 无效数据 - 密码强度不足
请求:
POST /users/register
Content-Type: application/json
{
"username": "john_doe",
"email": "john@example.com",
"password": "password",
"age": 25
}响应:
{
"statusCode": 400,
"message": ["Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number"],
"error": "Bad Request"
}5. 无效数据 - 年龄不符合要求
请求:
POST /users/register
Content-Type: application/json
{
"username": "john_doe",
"email": "john@example.com",
"password": "Password123",
"age": 17
}响应:
{
"statusCode": 400,
"message": ["You must be at least 18 years old"],
"error": "Bad Request"
}6. 无效数据 - 包含额外字段
请求:
POST /users/register
Content-Type: application/json
{
"username": "john_doe",
"email": "john@example.com",
"password": "Password123",
"age": 25,
"extraField": "value"
}响应:
{
"statusCode": 400,
"message": ["Extra properties are not allowed: extraField"],
"error": "Bad Request"
}代码解析
DTO创建:
- 创建了
CreateUserDto类,使用class-validator装饰器定义验证规则 - 为每个字段添加了详细的验证规则和错误消息
- 创建了
ValidationPipe配置:
- 在
main.ts中注册了全局ValidationPipe - 配置了
whitelist: true,自动删除未在DTO中定义的属性 - 配置了
forbidNonWhitelisted: true,当存在未定义的属性时抛出错误 - 配置了
transform: true,自动将请求数据转换为DTO类型 - 配置了
disableErrorMessages: false,显示详细的错误信息
- 在
控制器实现:
- 创建了
UsersController,处理用户注册请求 - 使用
@Body()装饰器获取请求体,并应用DTO类型
- 创建了
测试验证:
- 测试了各种有效和无效的请求数据
- 验证了管道能够正确处理数据验证和错误抛出
互动思考问题
思考:管道和中间件的区别是什么?它们各自的使用场景是什么?
讨论:在什么情况下应该使用内置管道,什么情况下应该创建自定义管道?
实践:尝试创建一个自定义管道,用于验证请求中的时间戳格式,并将其转换为Date对象。
挑战:如何创建一个管道,用于验证请求中的JWT令牌,并将解码后的数据添加到请求对象中?
扩展:了解NestJS的守卫(Guards),思考它与管道的区别和配合使用的场景。
小结
本集我们学习了NestJS管道的核心概念和使用方法,包括:
- 管道的基本概念和作用
- 内置管道的使用方法
- 数据验证的实现原理
- 类型转换的实现原理
- 自定义管道的创建和使用
- 管道的依赖注入
- 管道与异常过滤器的配合使用
通过实践案例,我们创建了一个完整的用户注册数据验证系统,展示了如何使用管道确保输入数据的合法性。管道是NestJS确保数据质量的重要组件,它使得数据验证和转换更加集中和可管理。
在下一集中,我们将学习NestJS的守卫(Guards),了解如何使用守卫进行权限控制和身份验证。