本文是关于使用 TypeScript、PostgreSQL 和 Prisma 构建后端的一系列直播和文章的一部分。在本文中,我们将探讨如何构建 REST API、验证输入以及测试 API。

简介
本系列的目标是通过解决一个具体问题来探索和演示现代后端的不同模式、问题和架构:在线课程评分系统。 这是一个很好的例子,因为它具有各种关系类型,并且足够复杂,可以代表真实的用例。
上述直播录像涵盖了与本文相同的内容。
本系列将涵盖的内容
本系列将重点关注数据库在后端开发各个方面的作用,涵盖:
您今天将学到什么
在第一篇文章中,您为问题域设计了一个数据模型,并编写了一个种子脚本,该脚本使用 Prisma Client 将数据保存到数据库中。
在本系列的第二篇文章中,您将基于第一篇文章中的数据模型和 Prisma schema 构建一个 REST API。您将使用 Hapi 来构建 REST API。通过 REST API,您将能够通过 HTTP 请求执行数据库操作。
作为 REST API 的一部分,您将开发以下方面:
- REST API: 实现一个带有资源端点的 HTTP 服务器,以处理不同模型的 CRUD。您将 Prisma 与 Hapi 集成,以便 API 端点处理程序可以访问 Prisma Client。
- 验证: 添加有效载荷验证规则,以确保用户输入与 Prisma schema 的预期类型匹配。
- 测试: 使用 Jest 和 Hapi 的
server.inject为 REST 端点编写测试,模拟 HTTP 请求,验证 REST 端点的验证和持久化逻辑。
在本文结束时,您将拥有一个包含 CRUD(创建、读取、更新和删除)操作端点和测试的 REST API。REST 资源将 HTTP 请求映射到 Prisma schema 中的模型,例如 GET /users 端点将处理与 User 模型相关的操作。
本系列的后续部分将详细介绍列表中的其他方面。
注意: 在整个指南中,您会发现各种检查点,它们使您能够验证是否正确执行了步骤。
先决条件
假定知识
本系列假定您对 TypeScript、Node.js 和关系数据库有基本了解。如果您有 JavaScript 经验但尚未尝试 TypeScript,您仍然可以继续学习。本系列将使用 PostgreSQL,但大多数概念也适用于其他关系数据库,如 MySQL。此外,熟悉 REST 概念也很有用。除此之外,不需要 Prisma 的先验知识,因为本系列将涵盖这些内容。
开发环境
您应该安装以下软件:
如果您正在使用 Visual Studio Code,建议安装 Prisma 扩展,用于语法高亮、格式化和其他辅助功能。
注意:如果您不想使用 Docker,可以设置一个本地 PostgreSQL 数据库或一个Heroku 托管 PostgreSQL 数据库。
克隆存储库
本系列的源代码可在 GitHub 上找到。
首先,克隆存储库并安装依赖项:
注意: 通过检出
part-2分支,您将能够从相同的起点开始阅读文章。
启动 PostgreSQL
要启动 PostgreSQL,请从 real-world-grading-app 文件夹中运行以下命令:
注意: Docker 将使用
docker-compose.yml文件启动 PostgreSQL 容器。
构建 REST API
在深入实现之前,我们将回顾一些与 REST API 相关的基本概念:
- API: 应用程序编程接口。一组允许程序相互通信的规则。通常,开发人员在服务器上创建 API,并允许客户端与之通信。
- REST: 一组开发人员遵循的约定,用于通过 HTTP 请求公开与状态相关的(在本例中为存储在数据库中的状态)操作。例如,请查看 GitHub REST API。
- 端点: REST API 的入口点,具有以下属性(非详尽):
- 路径,例如
/users/,用于访问用户端点。路径决定了用于访问端点的 URL,例如www.myapi.com/users/。 - HTTP 方法,例如
GET、POST和DELETE。HTTP 方法将决定端点公开的操作类型,例如GET /users端点将允许获取用户,POST /users端点将允许创建用户。 - 处理程序:将处理端点请求的代码(在本例中为 TypeScript)。
- 路径,例如
- HTTP 状态码: 响应 HTTP 状态码将告知 API 消费者操作是否成功以及是否发生任何错误。查看此列表以了解不同的 HTTP 状态码,例如资源成功创建时为
201,消费者输入验证失败时为400。
注意: REST 方法的关键目标之一是使用 HTTP 作为应用程序协议,以通过遵循约定来避免重复造轮子。
API 端点
API 将具有以下端点(HTTP 方法后跟路径):
UserPOST/users创建用户(并可选地与课程关联)UserGET/users/{userId}获取用户UserPUT/users/{userId}更新用户UserDELETE/users/{userId}删除用户UserGET/users获取用户CourseEnrollmentGET/users/{userId}/courses获取用户的课程注册CourseEnrollmentPOST/users/{userId}/courses将用户注册到课程(作为学生或教师)CourseEnrollmentDELETE/users/{userId}/courses/{courseId}删除用户在课程中的注册CoursePOST/courses创建课程CourseGET/courses获取课程CourseGET/courses/{courseId}获取课程CoursePUT/courses/{courseId}更新课程CourseDELETE/courses/{courseId}删除课程TestPOST/courses/{courseId}/tests为课程创建测试TestGET/courses/tests/{testId}获取测试TestPUT/courses/tests/{testId}更新测试TestDELETE/courses/tests/{testId}删除测试Test ResultGET/users/{userId}/test-results获取用户的测试结果Test ResultPOST/courses/tests/{testId}/test-results为与用户关联的测试创建测试结果Test ResultGET/courses/tests/{testId}/test-results获取测试的多个测试结果Test ResultPUT/courses/tests/test-results/{testResultId}更新测试结果(与用户和测试关联)Test ResultDELETE/courses/tests/test-results/{testResultId}删除测试结果注意: 包含用
{}括起来的参数(例如{userId})的路径表示在 URL 中插值的变量,例如在www.myapi.com/users/13中,userId是13。
以上端点已根据其关联的主要模型/资源进行分组。这种分类有助于将代码组织到单独的模块中,以提高可维护性。
在本文中,您将实现上述端点的一部分(前四个),以说明不同 CRUD 操作的不同模式。完整的 API 将在 GitHub 存储库中提供。这些端点应提供大多数操作的接口。虽然某些资源没有用于删除资源的 DELETE 端点,但可以在以后添加它们。
注意: 在本文中,endpoint 和 route 将互换使用。虽然它们指的是同一事物,但 endpoint 是在 REST 上下文中使用的术语,而 route 是在 HTTP 服务器上下文中使用的术语。
Hapi
API 将使用 Hapi 构建——一个用于构建 HTTP 服务器的 Node.js 框架,它开箱即用地支持验证和测试。
Hapi 由一个名为 @hapi/hapi 的核心模块(即 HTTP 服务器)和扩展核心功能的模块组成。在这个后端中,您还将使用以下模块:
@hapi/joi用于声明式输入验证@hapi/boom用于 HTTP 友好的错误对象
为了让 Hapi 与 TypeScript 协同工作,您需要添加 Hapi 和 Joi 的类型。这是因为 Hapi 是用 JavaScript 编写的。通过添加类型,您将拥有丰富的自动补全功能,并允许 TypeScript 编译器确保代码的类型安全。
安装以下包:
创建服务器
您需要做的第一件事是创建一个 Hapi 服务器,它将绑定到一个接口和端口。
将以下 Hapi 服务器添加到 src/server.ts:
首先,您导入 Hapi。然后,您使用包含要侦听的端口号和主机信息的连接详细信息初始化一个新的 Hapi.server()(类型为 Hapi.Server,在 @types/hapi__hapi 包中定义)。之后,您启动服务器并记录它正在运行。
要在开发期间在本地运行服务器,请运行 npm dev 脚本,它将使用 ts-node-dev 自动转译 TypeScript 代码并在您进行更改时重新启动服务器:npm run dev
检查点: 如果您在浏览器中打开 https://:3000,您应该会看到以下内容:{"statusCode":404,"error":"Not Found","message":"Not Found"}
恭喜,您已成功创建服务器。但是,服务器没有定义任何路由。在下一步中,您将定义第一个路由。
定义路由
要添加路由,您将在上一步实例化的 Hapi server 上使用 route() 方法。在定义与业务逻辑相关的路由之前,添加一个返回 200 HTTP 状态码的 /status 端点是一个好习惯。这对于确保服务器正常运行非常有用。
为此,请通过在 server.ts 中添加以下内容来更新 start 函数:
在这里,您定义了 HTTP 方法、路径和返回对象 { up: true } 的处理程序,最后将 HTTP 状态码设置为 200。
检查点: 如果您在浏览器中打开 https://:3000,您应该会看到以下内容:{"up":true}
将路由移动到插件
在上一步中,您定义了一个状态端点。由于 API 将公开许多不同的端点,因此将它们全部定义在 start 函数中将不可维护。
Hapi 具有插件的概念,作为将后端分解为独立的业务逻辑块的方式。插件是一种保持代码模块化的精益方式。在此步骤中,您将把上一步中定义的路由移动到插件中。
这需要两个步骤:
- 在新文件中定义一个插件。
- 在调用
server.start()之前注册插件
定义插件
首先在 src/ 中创建一个名为 plugins 的新文件夹。
在 src/plugins/ 文件夹中创建一个名为 status.ts 的新文件。
并向文件中添加以下内容:
Hapi 插件是一个包含 name 属性和 register 函数的对象,您通常会在其中封装插件的逻辑。name 属性是插件名称字符串,用作唯一键。
每个插件都可以通过标准 服务器接口 操作服务器。在上面的 app/status 插件中,server 用于在 register 函数中定义 status 路由。
注册插件
要注册插件,请返回 server.ts 并按如下方式导入状态插件:
在 start 函数中,将上一步中的 route() 调用替换为以下 server.register() 调用:
检查点: 如果您在浏览器中打开 https://:3000,您应该会看到以下内容:{"up":true}
恭喜,您已成功创建了一个封装状态端点逻辑的 Hapi 插件。
在下一步中,您将定义一个测试来测试状态端点。
为状态端点定义测试
为了测试状态端点,您将使用 Jest 作为测试运行器,并结合 Hapi 的 server.inject 测试辅助函数,该函数模拟对服务器的 HTTP 请求。这将允许您验证是否正确实现了端点。
将 server.ts 拆分为两个文件
要使用 server.inject 方法,您需要在测试中访问已注册插件但尚未启动服务器的 server 对象,以避免在测试运行时服务器侦听请求。为此,请修改 server.ts,使其如下所示:
您刚刚将 start 函数替换为两个函数:
createServer():注册插件并初始化服务器startServer():启动服务器
注意: Hapi 的
server.initialize()初始化服务器(启动缓存,完成插件注册),但不开始侦听连接端口。
现在,您可以在测试中导入 server.ts 并使用 createServer() 初始化服务器,然后调用 server.inject() 模拟 HTTP 请求。
接下来,您将为应用程序创建一个新的入口点,它将同时调用 createServer() 和 startServer()。
创建一个新的 src/index.ts 文件并向其中添加以下内容:
最后,更新 package.json 中的 dev 脚本,以启动 src/index.ts 而不是 src/server.ts。
创建测试
要创建测试,请在项目根目录中创建一个名为 tests 的文件夹,并创建一个名为 status.test.ts 的文件,并向其中添加以下内容:
在上面的测试中,beforeAll 和 afterAll 用作设置和拆卸函数,用于创建和停止服务器。
然后,调用 server.inject 来模拟对根端点 / 的 GET HTTP 请求。然后测试断言 HTTP 状态码和有效载荷以确保它与处理程序匹配。
检查点: 使用 npm test 运行测试,您应该会看到以下输出:
恭喜,您已创建了一个带有路由的插件并测试了该路由。
在下一步中,您将定义一个 Prisma 插件,以便您可以在整个应用程序中访问 Prisma Client 实例。
定义 Prisma 插件
与创建状态插件类似,为 Prisma 插件创建一个新文件 src/plugins/prisma.ts。
Prisma 插件的目标是实例化 Prisma Client,通过 server.app 对象使其可供应用程序的其他部分使用,并在服务器停止时断开与数据库的连接。server.app 提供了一个安全的地方来存储服务器特定的运行时应用程序数据,而不会与框架内部发生潜在冲突。只要服务器可访问,就可以访问数据。
将以下内容添加到 src/plugins/prisma.ts 文件中:
在这里,我们定义了一个插件,实例化了 Prisma Client,将其分配给 server.app,并添加了一个扩展函数(可以看作是一个钩子),该函数将在 onPostStop 事件发生时运行,该事件在服务器的连接监听器停止后被调用。
要注册 Prisma 插件,请在 server.ts 中导入该插件,并将其添加到传递给 server.register 调用的数组中,如下所示:
如果您正在使用 VSCode,您会在 src/plugins/prisma.ts 文件中的 server.app.prisma = prisma 下方看到一条红色波浪线。这是您遇到的第一个类型错误。如果您没有看到该行,可以运行 compile 脚本来运行 TypeScript 编译器。
出现此错误的原因是您修改了 server.app 而没有更新其类型。要解决此错误,请在 prismaPlugin 定义的顶部添加以下内容:
这将增强模块并将 PrismaClient 类型分配给 server.app.prisma 属性。
注意: 有关为什么模块增强是必要的更多信息,请查看 DefinitelyTyped 存储库中的此评论。
除了安抚 TypeScript 编译器之外,这还将使 server.app.prisma 在整个应用程序中被访问时自动补全功能正常工作。
检查点: 如果您再次运行 npm run compile,则不应发出任何错误。
做得好!您现在已经定义了两个插件,并使 Prisma Client 可用于应用程序的其余部分。在下一步中,您将为用户路由定义一个插件。
为用户路由定义插件,该插件依赖于 Prisma 插件
现在您将为用户路由定义一个新插件。此插件将需要使用您在 Prisma 插件中定义的 Prisma Client,以便在用户特定的路由处理程序中执行 CRUD 操作。
Hapi 插件有一个可选的 dependencies 属性,可用于指示对其他插件的依赖。指定后,Hapi 将确保插件以正确的顺序加载。
首先,为用户插件创建一个新文件 src/plugins/users.ts。
向文件中添加以下内容:
在这里,您将一个数组传递给 dependencies 属性,以确保 Hapi 首先加载 Prisma 插件。
现在您可以在 register 函数中定义用户特定的路由,因为您知道 Prisma Client 将可访问。
最后,您需要导入插件并将其注册到 src/server.ts 中,如下所示:
在下一步中,您将定义一个创建用户端点。
定义创建用户路由
定义用户插件后,您现在可以定义创建用户路由。
创建用户路由将具有 HTTP 方法 POST 和路径 /users。
首先在 src/plugins/users.ts 中 register 函数内部添加以下 server.route 调用:
然后按如下方式定义 createUserHandler 函数:
在这里,您从 server.app 对象(在 Prisma 插件中分配)访问 prisma,并在 prisma.user.create 调用中使用请求有效载荷将用户保存到数据库中。
您应该会再次在访问 payload 属性的行下方看到一条红色波浪线,表示类型错误。如果您没有看到该错误,请再次运行 TypeScript 编译器。
这是因为 payload 的值是在运行时确定的,因此 TypeScript 编译器无法知道其类型。这可以通过类型断言来解决。类型断言是 TypeScript 中的一种机制,允许您覆盖变量的推断类型。TypeScript 的类型断言纯粹是您告诉编译器您比它更了解类型,就像这里一样。
为此,为预期的有效载荷定义一个接口:
注意: TypeScript 中的类型和接口有许多相似之处。
然后添加类型断言:
插件应该如下所示:
向创建用户路由添加验证
在此步骤中,您还将使用 Joi 添加有效负载验证,以确保路由只处理具有正确数据的请求。
验证可以被认为是运行时类型检查。当使用 TypeScript 时,编译器执行的类型检查仅限于编译时已知的信息。由于用户 API 输入在编译时无法得知,因此运行时验证有助于解决此类情况。
为此,请按如下方式导入 Joi:
Joi 允许您通过创建 Joi 验证对象来定义验证规则,该对象可以分配给路由处理程序,以便 Hapi 将知道如何验证有效载荷。
在创建用户端点中,您希望验证用户输入是否符合您上面定义的类型:
Joi 对应的验证对象将如下所示:
接下来,您必须配置路由处理程序以使用验证器对象 userInputValidator。将以下内容添加到您的路由定义对象中:
为创建用户路由创建测试
在此步骤中,您将创建一个测试来验证创建用户逻辑。该测试将使用 server.inject 向 POST /users 端点发出请求,并检查响应是否包含 id 字段,从而验证用户已在数据库中创建。
首先创建一个 tests/users.tests.ts 文件,并添加以下内容:
该测试注入了一个带有有效负载的请求,并断言 statusCode 以及响应中的 id 是否为数字。
注意: 该测试通过确保
现在您已经为“快乐路径”(成功创建用户)编写了测试,您将编写另一个测试来验证验证逻辑。您将通过使用无效有效负载(例如,省略必填字段 firstName)来创建另一个请求,如下所示:
检查点: 使用 npm test 命令运行测试,并验证所有测试都通过。
定义和测试获取用户路由
在此步骤中,您将首先为获取用户端点定义一个测试,然后实现路由处理程序。
提醒一下,获取用户端点将具有 GET /users/{userId} 签名。
首先编写测试然后实现代码的做法通常被称为 测试驱动开发。测试驱动开发可以通过提供一种快速机制来验证更改的正确性,从而提高生产力。
定义测试
首先,您将测试路由在找不到用户时返回 404。
打开 users.test.ts 文件并添加以下 test:
第二个测试将测试成功检索用户的“快乐路径”。您将使用上一步中创建用户测试中设置的 userId 变量。这将确保您获取现有用户。添加以下测试:
由于您尚未定义路由,因此现在运行测试将导致测试失败。下一步将是定义路由。
定义路由
转到 users.ts(用户插件)并将以下路由对象添加到 server.route() 调用中:
与为创建用户端点定义验证规则类似,在上述路由定义中,您验证 userId URL 参数以确保传递的是数字。
接下来,定义 getUserHandler 函数,如下所示:
注意: 当调用
findUnique时,如果找不到结果,Prisma 将返回null。
在处理程序中,userId 从请求参数中解析并用于 Prisma Client 查询。如果找不到用户,则返回 404,否则返回找到的用户对象。
检查点: 使用 npm test 运行测试,并验证所有测试都已通过。
定义和测试删除用户路由
在此步骤中,您将为删除用户端点定义一个测试,然后实现路由处理程序。
删除用户端点将具有 DELETE /users/{userId} 签名。
定义测试
首先,您将为路由的参数验证编写一个测试。将以下测试添加到 users.test.ts:
然后添加另一个测试来测试删除用户逻辑,您将在其中删除在创建用户测试中创建的用户。
注意: 204 状态响应代码表示请求已成功,但响应没有内容。
定义路由
转到 users.ts(用户插件)并将以下路由对象添加到 server.route() 调用中:
定义路由后,按如下方式定义 deleteUserHandler:
检查点: 使用 npm test 运行测试,并验证所有测试都已通过。
定义和测试更新用户路由
在此步骤中,您将为更新用户端点定义一个测试,然后实现路由处理程序。
更新用户端点将具有 PUT /users/{userId} 签名。
为更新用户路由编写测试
首先,您将为路由的参数验证编写一个测试。将以下测试添加到 users.test.ts:
为更新用户端点添加另一个测试,您将在其中更新用户的 firstName 和 lastName 字段(针对在创建用户测试中创建的用户):
定义更新用户验证规则
在此步骤中,您将定义更新用户路由。在验证方面,端点的有效负载不应要求任何特定字段(与创建用户端点不同,其中 email、firstName 和 lastName 是必需的)。这将允许您使用端点更新单个字段,例如 firstName。
要定义有效负载验证,您可以(可以)使用 userInputValidator Joi 对象,但是,如果您回忆一下,某些字段是必需的。
在更新用户端点中,所有字段都应该是可选的。Joi 提供了一种使用 tailor 和 alter 方法创建相同 Joi 对象的不同变体的方式。这在定义具有相似验证规则的创建和更新路由时特别有用,同时保持代码的 DRY。
如下更新已定义的 userInputValidator:
更新创建用户路由的有效负载验证
现在您可以在 src/plugins/users.ts(用户插件)中更新创建用户路由定义以使用 createUserValidator:
定义更新用户路由
定义更新的验证对象后,您现在可以定义更新用户路由。转到 src/plugins/users.ts(用户插件)并将以下路由对象添加到 server.route() 调用中:
定义路由后,按如下方式定义 updateUserHandler 函数:
检查点: 使用 npm test 运行测试,并验证所有测试都已通过。
总结和后续步骤
如果您已经走到这一步,恭喜。本文涵盖了许多内容,从 REST 概念开始,然后深入探讨了 Hapi 概念,例如路由、插件、插件依赖项、测试和验证。
您实现了 Hapi 的 Prisma 插件,使 Prisma 可用于整个应用程序,并实现了利用它的路由。
此外,TypeScript 有助于自动完成和验证在整个应用程序中正确使用类型(与数据库模式同步)。
本文涵盖了所有端点子集的实现。作为下一步,您可以遵循相同的原则实现其他路由。
您可以在 GitHub 上找到后端的完整源代码。
本文的重点是实现 REST API,但是,验证和测试等概念也适用于其他情况。
虽然 Prisma 旨在简化关系数据库的操作,但更深入地了解底层数据库可能会有所帮助。
查看 Prisma 的数据指南,了解有关数据库工作原理、如何选择合适的数据库以及如何充分利用数据库的更多信息。
在本系列的下一部分中,您将了解更多关于:
- 身份验证:使用电子邮件和 JWT 实现无密码身份验证。
- 持续集成:构建 GitHub Actions 管道以自动化后端测试。
- 与外部 API 集成:使用事务性电子邮件 API 发送电子邮件。
- 授权:为不同资源提供不同级别的访问权限。
- 部署
不要错过下一篇文章!
订阅 Prisma 新闻通讯