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

简介
本系列的目标是通过解决一个具体的问题来探索和演示现代后端的不同模式、问题和架构:在线课程的评分系统。 这是一个很好的例子,因为它具有多样的关系类型,并且足够复杂,可以代表真实的用例。
直播的录像可在上方找到,内容与本文相同。
本系列将涵盖的内容
本系列将侧重于数据库在后端开发的各个方面的作用,涵盖
主题 | 部分 |
---|---|
数据建模 | 第 1 部分 |
CRUD | 第 1 部分 |
聚合 | 第 1 部分 |
REST API 层 | 第 2 部分(当前) |
验证 | 第 2 部分(当前) |
测试 | 第 2 部分(当前) |
身份验证 | 即将推出 |
授权 | 即将推出 |
与外部 API 集成 | 即将推出 |
部署 | 即将推出 |
您今天将学到的内容
在第一篇文章中,您为问题域设计了一个数据模型,并编写了一个种子脚本,该脚本使用 Prisma Client 将数据保存到数据库。
在本系列文章的第二篇中,您将在第一篇文章中的数据模型和 Prisma 模式之上构建 REST API。您将使用 Hapi 来构建 REST API。借助 REST API,您将能够通过 HTTP 请求执行数据库操作。
作为 REST API 的一部分,您将开发以下方面
- REST API: 实现一个 HTTP 服务器,其中包含资源端点,用于处理不同模型的 CRUD 操作。您将 Prisma 与 Hapi 集成,以便允许 API 端点处理程序访问 Prisma Client。
- 验证: 添加有效负载验证规则,以确保用户输入与 Prisma 模式的预期类型匹配。
- 测试: 使用 Jest 和 Hapi 的
server.inject
为 REST 端点编写测试,模拟 HTTP 请求,验证 REST 端点的验证和持久性逻辑。
在本文结束时,您将拥有一个 REST API,其中包含用于 CRUD(创建、读取、更新和删除)操作和测试的端点。REST 资源会将 HTTP 请求映射到 Prisma 模式中的模型,例如,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/
,用于访问 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 方法后跟路径)
资源 | HTTP 方法 | 路由 | 描述 |
---|---|---|---|
用户 | POST | /users | 创建用户(并可选择与课程关联) |
用户 | GET | /users/{userId} | 获取用户 |
用户 | PUT | /users/{userId} | 更新用户 |
用户 | DELETE | /users/{userId} | 删除用户 |
用户 | GET | /users | 获取用户列表 |
课程注册 | GET | /users/{userId}/courses | 获取用户的课程注册信息 |
课程注册 | POST | /users/{userId}/courses | POST |
课程注册 | DELETE | /users/{userId}/courses/{courseId} | 删除用户在课程中的注册 |
课程 | POST | POST | /courses |
课程 | GET | POST | 获取课程列表 |
课程 | GET | /courses/{courseId} | 获取课程 |
课程 | PUT | /courses/{courseId} | PUT |
课程 | DELETE | /courses/{courseId} | DELETE |
测试 | POST | POST | /courses/{courseId}/tests |
测试 | GET | /courses/tests/{testId} | 获取测试 |
测试 | PUT | /courses/tests/{testId} | PUT |
测试 | DELETE | /courses/tests/{testId} | DELETE |
测试结果 | GET | /users/{userId}/test-results | 获取用户的测试结果 |
测试结果 | POST | /courses/tests/{testId}/test-results | 为与用户关联的测试创建测试结果 |
测试结果 | GET | /courses/tests/{testId}/test-results | 获取测试的多个测试结果 |
测试结果 | PUT | /courses/tests/test-results/{testResultId} | 更新测试结果(与用户和测试关联) |
测试结果 | DELETE | /courses/tests/test-results/{testResultId} | DELETE |
注意: 包含在
{}
中的参数的路径,例如{userId}
,表示 URL 中插值的变量,例如在www.myapi.com/users/13
中,userId
是13
。
上面的端点已根据它们关联的主要模型/资源进行分组。这种分类将有助于将代码组织成单独的模块,以提高可维护性。
在本文中,您将实现上面端点的一个子集(前四个),以说明不同 CRUD 操作的不同模式。完整的 API 将在 GitHub 存储库中提供。这些端点应为大多数操作提供接口。虽然某些资源没有用于删除资源的 DELETE
端点,但它们可以稍后添加。
注意: 在整篇文章中,术语端点和路由将互换使用。虽然它们指的是同一件事,但端点是在 REST 上下文中使用的术语,而路由是在 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()
(类型为 @types/hapi__hapi
包中定义的 Hapi.Server
),其中包含要监听的端口号和主机信息。之后,您启动服务器并记录它正在运行。
要在开发期间在本地运行服务器,请运行 npm dev
脚本,该脚本将使用 ts-node-dev
自动转换 TypeScript 代码,并在您进行更改时重新启动服务器:npm run dev
检查点: 如果您在浏览器中打开 http://127.0.0.1:3000,您应该看到以下内容: {"statusCode":404,"error":"Not Found","message":"Not Found"}
恭喜,您已成功创建服务器。但是,服务器没有定义任何路由。在下一步中,您将定义第一个路由。
定义路由
要添加路由,您将使用您在上一步中实例化的 Hapi server
上的 route()
方法。在定义与业务逻辑相关的路由之前,最好添加一个 /status
端点,该端点返回 200
HTTP 状态代码。这对于确保服务器正常运行很有用
为此,请通过将以下内容添加到顶部来更新 server.ts
中的 start
函数
在这里,您定义了 HTTP 方法、路径和一个处理程序,该处理程序返回对象 { up: true }
,最后将 HTTP 状态代码设置为 200
。
检查点: 如果您在浏览器中打开 http://127.0.0.1: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
并按如下方式导入 status 插件
在 start
函数中,将上一步中的 route()
调用替换为以下 server.register()
调用
检查点: 如果您在浏览器中打开 http://127.0.0.1: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 将确保插件以正确的顺序加载。
首先为 users 插件创建一个新的 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
第二个测试将测试 happy path – 成功检索用户。您将使用在上一步创建的用户创建测试中设置的 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 新闻通讯