2022 年 7 月 19 日

使用 NestJS 和 Prisma 构建 REST API:输入验证和转换

8 分钟阅读

欢迎来到关于使用 NestJS、Prisma 和 PostgreSQL 构建 REST API 系列的第二篇教程!在本教程中,您将学习如何在 API 中执行输入验证和转换。

Building a REST API with NestJS and Prisma: Input Validation & Transformation

目录

简介

在本系列的第一部分中,您创建了一个新的 NestJS 项目,并将其与 Prisma、PostgreSQL 和 Swagger 集成。然后,您为博客应用程序的后端构建了一个基本的 REST API。

在这一部分中,您将学习如何验证输入,使其符合您的 API 规范。执行输入验证是为了确保只有格式正确的数据才能从客户端传递到您的 API。验证发送到 Web 应用程序的任何数据的正确性是最佳实践。这有助于防止格式错误的数据和 API 滥用。

您还将学习如何执行输入转换。输入转换是一种技术,允许您在客户端发送的数据被该请求的路由处理程序处理之前,拦截并转换这些数据。这对于将数据转换为适当的类型、将默认值应用于缺失字段、清理输入等非常有用。

开发环境

要学习本教程,您需要具备以下条件

  • 已安装 Node.js
  • 已安装 DockerPostgreSQL
  • 已安装 Prisma VSCode 扩展(可选)
  • 可以访问 Unix shell(例如 Linux 和 macOS 中的终端/shell)以运行本系列中提供的命令。(可选)

注意:

  1. 可选的 Prisma VS Code 扩展为 Prisma 添加了一些不错的 IntelliSense 和语法高亮。

  2. 如果您没有 Unix shell(例如,您在 Windows 机器上),您仍然可以继续学习,但 shell 命令可能需要针对您的机器进行修改。

克隆仓库

本教程的起点是本系列第一部分的结尾。它包含一个使用 NestJS 构建的基本 REST API。我建议您在开始本教程之前完成第一个教程。

本教程的起点位于 GitHub 仓库begin-validation 分支中。要开始,请克隆仓库并检出 begin-validation 分支

现在,执行以下操作以开始

  1. 导航到克隆的目录
  1. 安装依赖
  1. 使用 docker 启动 PostgreSQL 数据库
  1. 应用数据库迁移
  1. 启动项目

注意:步骤 4 还会生成 Prisma Client 并填充数据库。

现在,您应该能够访问 API 文档,地址为 http://localhost:3000/api/

项目结构和文件

您克隆的仓库应具有以下结构

此仓库中值得注意的文件和目录包括

  • src 目录包含应用程序的源代码。有三个模块
    • app 模块位于 src 目录的根目录,是应用程序的入口点。它负责启动 Web 服务器。
    • prisma 模块包含 Prisma Client,您的数据库查询构建器。
    • articles 模块定义了 /articles 路由的端点以及随附的业务逻辑。
  • prisma 模块具有以下内容
    • schema.prisma 文件定义了数据库模式。
    • migrations 目录包含数据库迁移历史记录。
    • seed.ts 文件包含一个脚本,用于使用虚拟数据填充您的开发数据库。
  • docker-compose.yml 文件定义了 PostgreSQL 数据库的 Docker 镜像。
  • .env 文件包含 PostgreSQL 数据库的数据库连接字符串。

注意:有关这些组件的更多信息,请阅读本教程系列的第一部分

执行输入验证

要执行输入验证,您将使用 NestJS Pipes。管道对路由处理程序正在处理的参数进行操作。Nest 在路由处理程序之前调用管道,管道接收目标为路由处理程序的参数。管道可以做很多事情,例如验证输入、向输入添加字段等。管道类似于中间件,但管道的范围仅限于处理输入参数。NestJS 提供了一些开箱即用的管道,但您也可以创建自己的自定义管道

管道有两个典型的用例

  • 验证:评估输入数据,如果有效,则保持不变地传递;否则,当数据不正确时抛出异常。
  • 转换:将输入数据转换为所需的格式(例如,从字符串到整数)。

NestJS 验证管道将检查传递给路由的参数。如果参数有效,管道会将参数传递给路由处理程序,而无需进行任何修改。但是,如果参数违反任何指定的验证规则,管道将抛出异常。

以下两个图表显示了验证管道如何工作,适用于任意 /example 路由。

在本节中,您将重点关注验证用例。

全局设置 ValidationPipe

要执行输入验证,您将使用内置的 NestJS ValidationPipeValidationPipe 提供了一种便捷的方法来为所有传入的客户端负载强制执行验证规则,其中验证规则使用 class-validator 包中的装饰器声明。

要使用此功能,您需要在项目中添加两个包

class-validator 包提供了用于验证输入数据的装饰器,而 class-transformer 包提供了用于将输入数据转换为所需格式的装饰器。这两个包都与 NestJS 管道良好集成。

现在,在您的 main.ts 文件中导入 ValidationPipe,并使用 app.useGlobalPipes 方法使其在您的应用程序中全局可用

CreateArticleDto 添加验证规则

您现在将使用 class-validator 包为 CreateArticleDto 添加验证装饰器。您将对 CreateArticleDto 应用以下规则

  1. title 不能为空或短于 5 个字符。
  2. description 的最大长度必须为 300。
  3. bodydescription 不能为空。
  4. titledescriptionbody 必须是 string 类型,而 published 必须是 boolean 类型。

打开 src/articles/dto/create-article.dto.ts 文件,并将其内容替换为以下内容

这些规则将被 ValidationPipe 拾取,并自动应用于您的路由处理程序。使用装饰器进行验证的优点之一是,CreateArticleDto 仍然是 POST /articles 端点所有参数的单一事实来源。因此,您无需定义单独的验证类。

测试您已设置的验证规则。尝试使用 POST /articles 端点创建一个文章,并使用非常短的占位符 title,如下所示

您应该收到 HTTP 400 错误响应,以及响应正文中关于哪个验证规则被破坏的详细信息。

HTTP 400 response with descriptive error message

此图表解释了 ValidationPipe 在幕后对 /articles 路由的无效输入所做的事情

Input validation flow with ValidationPipe

从客户端请求中剥离不必要的属性

CreateArticleDTO 定义了创建新文章需要发送到 POST /articles 端点的属性。UpdateArticleDTO 执行相同的操作,但用于 PATCH /articles/{id} 端点。

目前,对于这两个端点,都可以发送 DTO 中未定义的其他属性。这可能会导致不可预见的错误或安全问题。例如,您可以手动将无效的 createdAtupdatedAt 值传递给 POST /articles 端点。由于 TypeScript 类型信息在运行时不可用,您的应用程序将无法识别这些字段在 DTO 中不可用。

为了举例说明,请尝试将以下请求发送到 POST /articles 端点

通过这种方式,您可以注入无效值。在这里,您创建了一篇 updatedAt 值早于 createdAt 的文章,这没有意义。

为了防止这种情况,您需要从客户端请求中过滤掉任何不必要的字段/属性。幸运的是,NestJS 也为此提供了一个开箱即用的解决方案。您只需在应用程序中初始化 ValidationPipe 时传递 whitelist: true 选项即可。

将此选项设置为 true 后,ValidationPipe 将自动删除所有非白名单属性,其中“非白名单” 意味着没有任何验证装饰器的属性。重要的是要注意,此选项将过滤所有没有验证装饰器的属性,即使它们在 DTO 中定义了。

现在,传递给请求的任何其他字段/属性都将由 NestJS 自动剥离,从而防止先前显示的漏洞利用。

注意:NestJS ValidationPipe 是高度可配置的。所有可用的配置选项都在 NestJS 文档中记录。如有必要,您还可以为您的应用程序构建自定义验证管道

使用 ParseIntPipe 转换动态 URL 路径

在您的 API 内部,您当前接受 GET /articles/{id}PATCH /articles/{id}DELETE /articles/{id} 端点的 id 参数作为路径的一部分。NestJS 从 URL 路径中将 id 参数解析为字符串。然后,在传递给 ArticlesService 之前,该字符串在您的应用程序代码内部被转换为数字。例如,看一下 DELETE /articles/{id} 路由处理程序

由于 id 被定义为字符串类型,因此 Swagger API 也在生成的 API 文档中将此参数记录为字符串。这是不直观且不正确的。

您可以使用 NestJS 管道自动将 id 转换为数字,而不是在路由处理程序内部手动进行此转换。将内置的 ParseIntPipe 添加到这三个端点的控制器路由处理程序

ParseIntPipe 将拦截字符串类型的 id 参数,并在将其传递给相应的路由处理程序之前自动将其解析为数字。这还具有在 Swagger 中将 id 参数正确记录为数字的优势。

总结和最终评论

恭喜!在本教程中,您使用了一个现有的 REST API 并进行了以下操作

  • 集成了使用 ValidationPipe 的验证。
  • 从客户端请求中剥离了不必要的属性。
  • 集成了 ParseIntPipe 以解析 string 路径变量并将其转换为 number

您可能已经注意到 NestJS 严重依赖装饰器。这是一个非常刻意的设计选择。NestJS 旨在通过大量使用装饰器来处理各种横切关注点,从而提高代码可读性和模块化。因此,控制器和服务方法不需要因验证、缓存、日志记录等样板代码而变得臃肿。

您可以在 GitHub 仓库end-validation 分支中找到本教程的完成代码。如果您发现问题,请随时在仓库中提出 issue 或提交 PR。您也可以在 Twitter 上直接与我联系。

不要错过下一篇文章!

注册 Prisma 新闻通讯