NestJS管道 (Pipes)

学习目标

  • 理解管道在NestJS中的作用和地位
  • 掌握内置管道的使用方法
  • 学会创建和使用自定义管道
  • 理解数据验证和类型转换的实现原理
  • 能够应用管道确保输入数据的合法性

核心知识点

1. 管道概念

管道是NestJS中用于数据处理的组件,它们可以:

  • 转换数据:将输入数据转换为所需的格式
  • 验证数据:确保输入数据符合特定的规则
  • 处理错误:当数据无效时抛出异常

管道在请求处理流程中的位置:

  1. 客户端发送请求
  2. 中间件处理请求
  3. 守卫验证权限
  4. 管道处理数据
  5. 控制器处理请求
  6. 服务执行业务逻辑
  7. 拦截器处理响应
  8. 异常过滤器处理异常
  9. 响应返回给客户端

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. 管道与异常过滤器

管道抛出的异常会被异常过滤器捕获,因此可以结合使用:

  1. 管道负责验证数据并抛出异常
  2. 异常过滤器负责统一处理异常并返回响应

实践案例分析

案例:用户注册数据验证

需求分析

我们需要创建一个用户注册API,要求:

  • 验证用户名:必须是字符串,长度在3-20个字符之间
  • 验证邮箱:必须是有效的邮箱格式
  • 验证密码:必须至少8个字符,包含至少一个大写字母、一个小写字母和一个数字
  • 验证年龄:必须是数字,在18-100之间
  • 提供友好的错误信息

实现步骤

  1. 创建用户注册DTO
  2. 配置ValidationPipe
  3. 创建控制器处理注册请求
  4. 测试验证功能

代码实现

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"
}

代码解析

  1. DTO创建

    • 创建了CreateUserDto类,使用class-validator装饰器定义验证规则
    • 为每个字段添加了详细的验证规则和错误消息
  2. ValidationPipe配置

    • main.ts中注册了全局ValidationPipe
    • 配置了whitelist: true,自动删除未在DTO中定义的属性
    • 配置了forbidNonWhitelisted: true,当存在未定义的属性时抛出错误
    • 配置了transform: true,自动将请求数据转换为DTO类型
    • 配置了disableErrorMessages: false,显示详细的错误信息
  3. 控制器实现

    • 创建了UsersController,处理用户注册请求
    • 使用@Body()装饰器获取请求体,并应用DTO类型
  4. 测试验证

    • 测试了各种有效和无效的请求数据
    • 验证了管道能够正确处理数据验证和错误抛出

互动思考问题

  1. 思考:管道和中间件的区别是什么?它们各自的使用场景是什么?

  2. 讨论:在什么情况下应该使用内置管道,什么情况下应该创建自定义管道?

  3. 实践:尝试创建一个自定义管道,用于验证请求中的时间戳格式,并将其转换为Date对象。

  4. 挑战:如何创建一个管道,用于验证请求中的JWT令牌,并将解码后的数据添加到请求对象中?

  5. 扩展:了解NestJS的守卫(Guards),思考它与管道的区别和配合使用的场景。

小结

本集我们学习了NestJS管道的核心概念和使用方法,包括:

  • 管道的基本概念和作用
  • 内置管道的使用方法
  • 数据验证的实现原理
  • 类型转换的实现原理
  • 自定义管道的创建和使用
  • 管道的依赖注入
  • 管道与异常过滤器的配合使用

通过实践案例,我们创建了一个完整的用户注册数据验证系统,展示了如何使用管道确保输入数据的合法性。管道是NestJS确保数据质量的重要组件,它使得数据验证和转换更加集中和可管理。

在下一集中,我们将学习NestJS的守卫(Guards),了解如何使用守卫进行权限控制和身份验证。

« 上一篇 NestJS异常过滤器 (Exception Filters) 下一篇 » NestJS守卫 (Guards)