14个文本转图像AI API
使用NestJS和Prisma构建REST API:输入验证和转换
欢迎来到使用 NestJS、Prisma 和 PostgreSQL 构建 REST API 系列教程的第二个教程!在本教程中,您将学习如何在 API 中执行输入数据的验证和转换。
介绍
在本系列教程的第一部分中,您已经成功创建了一个新的 NestJS 项目,并将其与 Prisma、PostgreSQL 数据库以及 Swagger 进行了集成。在此基础上,您为博客应用程序的后端构建了一个基础的 REST API。
接下来的这一部分教程,将引导您学习如何验证输入数据,以确保其符合 API 规范。执行输入验证是至关重要的,因为它能够确保只有格式正确的数据才能通过 API 传递给后端。最佳实践是,对发送到 Web 应用程序的任何数据都进行正确性验证。这样做有助于防止因数据格式错误而导致的潜在问题,并防止滥用您的 API。
此外,您还将学习如何执行输入转换。输入转换是一种在数据到达路由处理程序之前,对其进行拦截和转换的技术。这对于将数据转换为适当的类型、为缺失的字段应用默认值、清理输入数据等场景非常有用。
开发环境
要学习本教程,您需要具备以下条件:
- 已安装 Node.js。
- 已安装 Docker 或 PostgreSQL 数据库。
- (可选)已安装 Prisma VSCode 扩展,以提升开发体验。
- (可选)能够访问 Unix shell(例如在 Linux 和 macOS 中的 terminal/shell),以便运行本系列教程中提供的命令。
注意:
- Prisma 提供了一个可选的 VS Code 扩展,该扩展为 Prisma 脚本添加了 IntelliSense 和语法高亮功能,从而提升了编码体验。
- 若您的计算机未配备 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
现在,执行以下操作以开始使用:
- 导航到克隆的目录:
cd blog-backend-rest-api-nestjs-prisma
- 安装依赖项:
npm install
- 使用 docker 启动 PostgreSQL 数据库:
docker-compose up -d
- 应用数据库迁移:
npx prisma migrate dev
- 启动项目:
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 内置的 ValidationPipe
。ValidationPipe
提供了一种高效的方式,可以对所有传入的客户端请求有效负载自动执行验证规则。这些验证规则是通过 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”)。title
、description
和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 错误响应,并在响应正文中收到有关违反了验证规则的详细信息。
下图展示了 ValidationPipe 在后台针对 /articles
路由的无效输入所执行的操作。
从客户端请求中去除不必要的属性
这确保 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"
}
这样,您可能会不小心注入无效值。例如,您可能会创建一篇文章,其中包含了像 precedes
、updatedAt
或 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