所有文章 > 如何集成API > 使用NestJS和Prisma构建REST API:输入验证和转换

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

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

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

介绍

在本系列教程的第一部分中,您已经成功创建了一个新的 NestJS 项目,并将其与 Prisma、PostgreSQL 数据库以及 Swagger 进行了集成。在此基础上,您为博客应用程序的后端构建了一个基础的 REST API。

接下来的这一部分教程,将引导您学习如何验证输入数据,以确保其符合 API 规范。执行输入验证是至关重要的,因为它能够确保只有格式正确的数据才能通过 API 传递给后端。最佳实践是,对发送到 Web 应用程序的任何数据都进行正确性验证。这样做有助于防止因数据格式错误而导致的潜在问题,并防止滥用您的 API。

此外,您还将学习如何执行输入转换。输入转换是一种在数据到达路由处理程序之前,对其进行拦截和转换的技术。这对于将数据转换为适当的类型、为缺失的字段应用默认值、清理输入数据等场景非常有用。

开发环境

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

  • 已安装 Node.js。
  • 已安装 Docker 或 PostgreSQL 数据库。
  • (可选)已安装 Prisma VSCode 扩展,以提升开发体验。
  • (可选)能够访问 Unix shell(例如在 Linux 和 macOS 中的 terminal/shell),以便运行本系列教程中提供的命令。

注意

  1. Prisma 提供了一个可选的 VS Code 扩展,该扩展为 Prisma 脚本添加了 IntelliSense 和语法高亮功能,从而提升了编码体验。
  2. 若您的计算机未配备 Unix shell(例如,您正在使用 Windows 系统),您仍然可以继续学习本教程,但可能需要根据您的系统环境对 shell 命令进行相应的调整。

克隆存储库

本教程的起点建立在系列教程第一部分的结束之处。它提供了一个基于 NestJS 构建的简单 REST API 作为基础。为了确保您能顺利跟上本教程的节奏,我们建议您先完成第一个教程。

本教程的起始代码位于 GitHub 存储库的 begin-validation 分支中。要开始本教程的学习,请首先克隆该存储库,并切换到 begin-validation 分支。

git clone -b begin-validation git@github.com:prisma/blog-backend-rest-api-nestjs-prisma.git

现在,执行以下操作以开始使用:

  1. 导航到克隆的目录:
cd blog-backend-rest-api-nestjs-prisma
  1. 安装依赖项:
npm install
  1. 使用 docker 启动 PostgreSQL 数据库:
docker-compose up -d
  1. 应用数据库迁移:
npx prisma migrate dev
  1. 启动项目:
npm run start:dev

注意:步骤 4 还将生成 Prisma Client 并设定数据库种子。

现在,您应该能够在以下位置访问 API 文档:http://localhost:3000/api/.

项目结构和文件

您克隆的存储库应具有以下结构:

median
├── node_modules
├── prisma
│ ├── migrations
│ ├── schema.prisma
│ └── seed.ts
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── main.ts
│ ├── articles
│ └── prisma
├── test
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── README.md
├── .env
├── docker-compose.yml
├── nest-cli.json
├── package-lock.json
├── package.json
├── tsconfig.build.json
└── tsconfig.json

此存储库中值得关注的文件和目录包括:

  • src 目录:该目录容纳了应用程序的源代码。具体而言,它包含了以下三个关键模块:
    • app 模块:位于目录的根位置,是应用程序的启动点,负责初始化并启动 Web 服务器。
    • prisma 模块:此模块集成了 Prisma Client,它是您的数据库查询生成工具。
    • articles 模块:定义了路由的端点以及相关的业务逻辑。
  • prisma 目录:虽然名称与上述模块相同,但此目录专门用于存放与 Prisma 相关的文件。其中包括:
    • schema.prisma 文件:该文件详细描述了数据库架构。
    • migrations 目录:用于记录数据库的迁移历史。
  • seed.ts 文件:此文件包含一个脚本,旨在使用模拟数据为开发数据库进行初始化设定。
  • docker-compose.yml 文件:该文件定义了 PostgreSQL 数据库的 Docker 映像配置。
  • .env 文件:包含了 PostgreSQL 数据库的连接字符串信息。

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

执行输入验证

为了执行输入验证,您将借助 NestJS 中的管道功能。管道针对路由处理程序正在处理的参数进行操作。在路由处理程序之前,Nest 会调用管道,而管道则接收发往路由处理程序的参数。管道的功能多样,不仅可以验证输入,还可以向输入添加字段等。尽管 NestJS 提供了一些内置的管道,但您同样可以创建自定义管道以满足特定需求。

管道有两个典型的用例:

  • 验证:评估输入数据的有效性。如果数据有效,则直接将其传递给路由处理程序;如果数据无效,则抛出异常。
  • Transformation(转换):将输入数据转换为所需的形式(例如,从字符串转换为整数)。

NestJS 的验证管道会检查传递给路由的参数。若参数有效,管道会将其原封不动地传递给路由处理程序;若参数违反任何指定的验证规则,管道则会抛出异常。

以下示意图展示了验证管道在任意路由(如 /example)中的工作原理。

在本节中,您将重点介绍验证使用案例。

全局设置 ValidationPipe

为了执行输入验证,您将利用 NestJS 内置的 ValidationPipeValidationPipe 提供了一种高效的方式,可以对所有传入的客户端请求有效负载自动执行验证规则。这些验证规则是通过 class-validator 包中的装饰器来声明的。

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

npm install class-validator class-transformer

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

现在,您需要在 main.ts 文件中导入这些包,并通过 app.useGlobalPipes 方法使 ValidationPipe 在您的应用程序中全局可用。

// src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

app.useGlobalPipes(new ValidationPipe());

const config = new DocumentBuilder()
.setTitle('Median')
.setDescription('The Median API description')
.setVersion('0.1')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);

await app.listen(3000);
}
bootstrap();

将验证规则添加到CreateArticDto

现在,您需要使用 class-validator 包中的验证修饰器来增强 CreateArticleDto。以下是您需要应用的验证规则:

  • title 不能为空,且长度不能少于 5 个字符。
  • description 的最大长度必须为 300 个字符。
  • body 不能为空(注意:原文中提到的“description”应为笔误,根据上下文应为“body”)。
  • titledescription 和 body 的类型分别为 string
  • 添加一个名为 published 的属性,其类型为 boolean

接下来,请打开 src/articles/dto/create-article.dto.ts 文件,并将其内容替换为符合上述规则的代码。

// src/articles/dto/create-article.dto.ts

import { ApiProperty } from '@nestjs/swagger';
import {
IsBoolean,
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
MinLength,
} from 'class-validator';

export class CreateArticleDto {
@IsString()
@IsNotEmpty()
@MinLength(5)
@ApiProperty()
title: string;

@IsString()
@IsOptional()
@IsNotEmpty()
@MaxLength(300)
@ApiProperty({ required: false })
description?: string;

@IsString()
@IsNotEmpty()
@ApiProperty()
body: string;

@IsBoolean()
@IsOptional()
@ApiProperty({ required: false, default: false })
published?: boolean = false;
}

这些验证规则将被自动选取并应用于您的路由处理程序。使用装饰器进行验证的一个显著优点是,this 关键字仍然是端点所有参数的单一事实来源,因此您无需定义单独的验证类。例如,对于 CreateArticleDto 在 POST /articles 路由中的应用,验证规则会自动生效。

为了测试您设置的验证规则,您可以尝试通过端点创建一个文章,但故意使用非常短的占位符作为标题,如下所示:POST /articles 请求体中仅包含 title 字段且其值很短。

{
"title": "Temp",
"description": "Learn about input validation",
"body": "Input validation is...",
"published": false
}

您应该会收到 HTTP 400 错误响应,并在响应正文中收到有关违反了验证规则的详细信息。

带有描述性错误消息的 HTTP 400 响应

下图展示了 ValidationPipe 在后台针对 /articles 路由的无效输入所执行的操作。

使用 ValidationPipe 的输入验证流程

从客户端请求中去除不必要的属性

这确保 API 安全和稳定的重要步骤。这定义了创建新文章时需要发送到端点的必要属性。对于创建文章的端点(POST /articles),我们使用 CreateArticleDTO 来明确这些属性。同样地,对于更新文章的端点(PATCH /articles/{id}),我们使用 UpdateArticleDTO 来指定哪些属性可以更新。

然而,目前这两个端点存在一个潜在问题:客户端可以发送 DTO 中未定义的额外属性。这可能会引发不可预见的错误或构成安全风险。例如,攻击者可能会尝试传递无效的值或恶意字段给终端节点。由于 TypeScript 的类型信息在运行时是不可用的,您的应用程序无法自动识别并拒绝这些不在 DTO 中定义的字段。

举个例子,尝试向POST /articles终端节点发送以下请求:

{
"title": "example-title",
"description": "example-description",
"body": "example-body",
"published": true,
"createdAt": "2010-06-08T18:20:29.309Z",
"updatedAt": "2021-06-02T18:20:29.310Z"
}

这样,您可能会不小心注入无效值。例如,您可能会创建一篇文章,其中包含了像 precedesupdatedAt 或 createdAt 这样没有实际意义或不应该由客户端指定的字段/属性。

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

// src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

app.useGlobalPipes(new ValidationPipe({ whitelist: true }));

const config = new DocumentBuilder()
.setTitle('Median')
.setDescription('The Median API description')
.setVersion('0.1')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);

await app.listen(3000);
}
bootstrap();

将此选项设置为 true 后,ValidationPipe 会自动移除所有未列入白名单的属性。这里的“non-whitelisted”指的是那些没有添加验证装饰器的属性。请注意,此选项会过滤掉所有没有验证装饰器的属性,即使它们在 DTO(数据传输对象)中已被定义。

现在,当客户端请求包含任何未经验证的字段/属性时,NestJS 会自动剥离这些字段,从而避免之前提到的安全漏洞。

注意:NestJS 提供了高度的可配置性。所有可用的配置选项都详细记录在 NestJS 官方文档中。如果标准配置无法满足您的需求,您还可以为应用程序构建自定义的验证管道。

转换动态 URL 路径中的参数:ParseIntPipe

在您的 API 中,GET /articles/{id}PATCH /articles/{id} 和 DELETE /articles/{id} 这三个端点目前都接受一个 id 参数作为 URL 路径的一部分。NestJS 默认会将路径中的参数解析为字符串。然而,在将这些参数传递给 ArticlesService 之前,您需要在应用程序代码中将这些字符串强制转换为数字。为了简化这一过程,您可以使用 ParseIntPipe。这个管道会自动将路径参数从字符串转换为整数,从而确保类型安全并减少出错的可能性。

// src/articles/articles.controller.ts

@Delete(':id')
@ApiOkResponse({ type: ArticleEntity })
remove(@Param('id') id: string) { // id is parsed as a string
return this.articlesService.remove(+id); // id is converted to number using the expression '+id'
}

由于 id 被定义为字符串类型,Swagger API 在生成的 API 文档中也会将此参数标记为字符串类型。然而,这种表示可能不够直观,甚至在某些情况下是不正确的,特别是当 id 实际上应该表示一个数字或特定格式的标识符时。

您可以在 NestJS 中使用管道来自动将 id 转换为数字,而无需在路由处理程序中手动执行此转换。为此,您可以将 ParseIntPipe(一个内置管道)添加到以下三个端点的控制器路由处理程序中,以便对 id 参数进行自动转换。

// src/articles/articles.controller.ts

import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
NotFoundException,
ParseIntPipe,
} from '@nestjs/common';

export class ArticlesController {
// ...

@Get(':id')
@ApiOkResponse({ type: ArticleEntity })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.articlesService.findOne(id);
}

@Patch(':id')
@ApiCreatedResponse({ type: ArticleEntity })
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateArticleDto: UpdateArticleDto,
) {
return this.articlesService.update(id, updateArticleDto);
}

@Delete(':id')
@ApiOkResponse({ type: ArticleEntity })
remove(@Param('id', ParseIntPipe) id: number) {
return this.articlesService.remove(id);
}
}

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

总结和结束语

祝贺您!在本教程中,您对一个现有的 REST API 进行了增强,具体完成了以下任务:

  • 使用了 ValidationPipe 来自动去除客户端请求中不必要的属性。
  • 集成了 ParseIntPipe,以便将路径变量从字符串解析并转换为数字。

您可能已经发现,NestJS 大量使用了装饰器。这是其设计中的一个核心特点,旨在通过装饰器来处理各种横切关注点,从而提高代码的可读性和模块化程度。因此,在控制器和服务方法中,您无需编写大量的样板代码来执行验证、缓存、日志记录等操作。

您可以在 GitHub 存储库的 end-validation 分支中找到本教程的完整代码。如果您在代码实现过程中遇到任何问题,欢迎在存储库中提出疑问或提交 Pull Request。当然,您也可以直接在 Twitter 上与我取得联系。

原文链接:https://www.prisma.io/blog/nestjs-prisma-validation-7D056s1kOla1

#你可能也喜欢这些API文章!