用Hapi和TypeScript为Jamstack构建一个Rest API
的Jamstack有一种很好的方法可以将前端和后端分开,这样整个解决方案就不必在一个单一的巨石中发布了——而且所有这些都是在同一时间完成的。当Jamstack与REST API配对时,客户端和API可以发展独立.这意味着前端和后端不是紧密耦合的,改变其中一个并不一定意味着改变另一个。
在本文中,我将从Jamstack的角度看一看REST API。我将展示如何在不破坏现有客户机的情况下改进API并遵循REST标准。我将选择Hapi作为构建API的工具,而Joi用于端点验证。数据库持久化层将通过Mongoose进入MongoDB来访问数据。测试驱动开发将帮助我迭代更改,并提供一种快速获得反馈的方法,同时减少认知负荷。最后,我们的目标是让您了解REST和Jamstack如何在软件模块之间提供高内聚和低耦合的解决方案。这种类型的体系结构最适合具有大量微服务的分布式系统,每个微服务都在自己的独立域中。我将假设具有NPM、ES6+的工作知识,并基本熟悉API端点。
该API将与作者数据一起工作,包括姓名、电子邮件和可选的1:N (一对几通过文档嵌入)在喜欢的话题上的关系。我将编写GET、PUT(带upsert)和DELETE端点。为了测试API,任何支持fetch ()
就行了,我来选Hoppscotch和旋度。
我将把这篇文章的阅读流程作为一个教程,你可以从头到尾跟着我读。对于那些宁愿跳过代码的人来说,它是可以在GitHub上找到供您观赏。本教程假设Node的工作版本(最好是最新的LTS)和MongoDB已安装.
初始设置
要从头开始项目,请创建一个文件夹和cd
成:
mkdirhapi-authors-rest-apicdhapi-authors-rest-api
进入项目文件夹后,启动npm init
按照提示操作。这将创建一个package.json
在文件夹的根目录。
每个Node项目都有依赖关系。我需要Hapi, Joi和Mongoose开始:
npm我@hapi/hapi joi猫鼬——save-exact
- @hapi /哈皮神: HTTP REST服务器框架
- 对未来:强大的对象模式验证器
- 猫鼬: MongoDB对象文档建模
检查package.json
确保所有依赖项和项目设置都到位。然后,在这个项目中添加一个入口点:
“脚本”:{“开始”:“节点index.js”},
带有版本的MVC文件夹结构
对于这个REST API,我将使用带有控制器、路由和数据库模型的典型MVC文件夹结构。控制器将有一个类似的版本AuthorV1Controller
允许API在模型发生突破性变化时进行演化。幸福就会有一个server.js
而且index.js
通过测试驱动开发使该项目可测试。的测验
文件夹将包含单元测试。
下面是文件夹的整体结构:
┳┣━┓配置┃┣━━dev.json┃┗━━index.js┣━┓控制器┃┗━━AuthorV1Controller.js┣━┓┃模型┣━━Author.js┃┗━━index.js┣━┓┃路线┣━━authors.js┃┗━━index.js┣━┓测试┃┗━━Author.js┣━━index.js┣━━包。Json -━━server.js
现在,继续在每个文件夹中创建文件夹和各自的文件。
mkdir配置控制器模型路由测验触摸配置/ dev.json配置/ index.js控制器/ AuthorV1Controller.js模型/Author.js model/index.js routes/authors.js routes/index.js test/Authors.js index.js server.js
这是每个文件夹的目的:
配置
:配置信息,以插入到Mongoose连接和Hapi服务器。控制器
:这些是处理请求/响应对象的Hapi处理程序。版本控制允许每个版本号有多个端点,也就是说,/ v1 /作者
,/ v2 /作者
等。模型
:连接MongoDB数据库,定义Mongoose模式。路线
:为REST纯粹主义者定义了使用Joi验证的端点。测验
:通过Hapi的实验室工具进行单元测试。(稍后再详细介绍。)
在实际项目中,您可能会发现将常见业务逻辑抽象到一个单独的文件夹中是很有用的跑龙套
.我建议创建一个AuthorUtil.js
模块与纯功能代码,使此可跨端点重用和易于单元测试。因为这个解决方案没有任何复杂的业务逻辑,所以我选择跳过这个文件夹。
添加更多文件夹的一个缺点是在进行更改时有更多的抽象层和更多的认知负荷。对于异常庞大的代码库,很容易迷失在混乱的误导层中。有时最好保持文件夹结构尽可能简单和平坦。
打印稿
为了改善开发人员的体验,我现在将添加TypeScript类型声明。因为Mongoose和Joi在运行时定义模型,所以在编译时添加类型检查器没有什么价值。在TypeScript中,可以将类型定义添加到普通JavaScript项目中,同时仍然可以在代码编辑器中获得类型检查器的好处。像WebStorm或VS Code这样的工具会选择类型定义,并允许程序员在代码中“点”。这种技术通常被称为智能感知,并且当IDE有可用的类型时启用它。这是一种定义编程接口的好方法,这样开发人员就可以在不查看文档的情况下点入对象。当开发人员点入错误的对象时,编辑器有时也会显示警告。
这是智能感知在VS Code中的样子:
在WebStorm中,这被称为代码完成,但本质上是一样的。您可以随意选择您喜欢的IDE来编写代码。我使用Vim和WebStorm,但你可以选择不同的。
要在这个项目中启用TypeScript类型声明,启动NPM并保存这些开发者依赖项:
npmI @types/hapi @types/mongoose—save-dev
我建议将开发人员依赖关系与应用程序依赖关系分开。这样,组织中的其他开发人员就很清楚这些包的用途。当构建服务器下拉repo时,它还可以选择跳过项目在运行时不需要的包。
所有开发人员的细节都到位之后,现在是时候开始编写代码了。打开Hapiserver.js
归档并放置主服务器:
常量配置=需要(”。/配置)常量路线=需要(“/路线。”)常量db=需要(”。/模型”)常量哈皮神=需要(“@hapi /哈皮神”)常量服务器=哈皮神.服务器({港口:配置.APP_PORT,宿主:配置.APP_HOST,路线:{歌珥:真正的}})服务器.路线(路线)出口.初始化=异步()= >{等待服务器.初始化()等待db.连接()返回服务器}出口.开始=异步()= >{等待服务器.开始()等待db.连接()控制台.日志(`服务器运行在:$ {服务器.信息.uri}`)返回服务器}过程.在(“unhandledRejection”,(犯错)= >{控制台.错误(犯错)过程.退出(1)})
我已经通过设置启用了CORS歌珥
为true,所以这个REST API可以与Hoppscotch一起工作。
为了简单起见,我在这个项目中不使用分号。跳过一个有点自由TypeScript构建在这个项目中然后输入额外的字符。这遵循了Hapi的原则,因为这是关于开发者的快乐。
下配置/ index.js
,一定要导出dev.json
信息:
模块.出口=需要(“/ dev。”)
要充实服务器的配置,可以添加这个dev.json
:
{“APP_PORT”:3000,“APP_HOST”:“127.0.0.1”}
其他验证
为了使REST端点遵循HTTP标准,我将添加Joi验证。这些验证有助于将API与客户机分离,因为它们强制执行资源完整性。对于Jamstack,这意味着客户端不再关心每个资源背后的实现细节。可以独立地处理每个端点,因为验证将确保对资源的有效请求。坚持严格的HTTP标准使客户端基于位于HTTP边界后面的目标资源进行演进,从而强制解耦。实际上,我们的目标是使用版本控制和验证来在Jamstack中保持一个清晰的边界。
使用REST,主要目标是维护幂等性使用GET、PUT和DELETE方法。这些是安全的请求方法,因为对同一资源的后续请求没有任何副作用。即使客户端未能建立连接,也会重复相同的预期效果。
我将选择跳过POST和PATCH,因为这些都不是安全的方法。这是为了简洁和幂等性,而不是因为这些方法以任何方式紧耦合客户端。同样严格的HTTP标准也适用于这些方法,除了它们不能保证幂等性。
在路线/ authors.js
,添加以下Joi验证:
常量对未来=需要(“未来”)常量authorV1Params=对未来.对象({id:对未来.字符串().要求()})常量authorV1Schema=对未来.对象({的名字:对未来.字符串().要求(),电子邮件:对未来.字符串().电子邮件().要求(),主题:对未来.数组().项目(对未来.字符串()),/ /可选createdAt:对未来.日期().要求()})
注意,对版本化模型的任何更改都可能需要一个新版本,比如v2
.这保证了现有客户端的向后兼容性,并允许API独立地发展。当缺少字段时,必填字段将以400(坏请求)响应失败请求。
设置好参数和模式验证后,向该资源添加实际路由:
/ /线路/ authors.js常量v1Endpoint=需要(“. . /控制器/ AuthorV1Controller”)模块.出口=[{方法:“得到”,路径:“/ v1 /作者/ {id}”,处理程序:v1Endpoint.细节,选项:{验证:{参数个数:authorV1Params},响应:{模式:authorV1Schema}}},{方法:“把”,路径:“/ v1 /作者/ {id}”,处理程序:v1Endpoint.插入,选项:{验证:{参数个数:authorV1Params,有效载荷:authorV1Schema},响应:{模式:authorV1Schema}}},{方法:“删除”,路径:“/ v1 /作者/ {id}”,处理程序:v1Endpoint.删除,选项:{验证:{参数个数:authorV1Params}}}]
使这些路线可用server.js
把这个加进去路线/ index.js
:
模块.出口=[...需要(”。/作者的)]
Joi验证在选项
路由数组的字段。属性对应的字符串ID参数ObjectId
在MongoDB。这id
是版本化路由的一部分,因为它是客户机需要使用的目标资源。对于PUT,有一个与GET响应匹配的有效负载验证。这是为了坚持REST标准的地方PUT响应必须匹配后续的GET.
标准是这么说的:
对给定表示的成功PUT将表明对同一目标资源的后续GET将导致在200 (OK)响应中发送等效表示。
这使得PUT不适合支持部分更新,因为后续的GET将不匹配PUT。对于Jamstack来说,遵循HTTP标准以确保客户端和解耦的可预测性是很重要的。
的AuthorV1Controller
中的方法处理程序处理请求v1Endpoint
.每个版本都有一个控制器是个好主意,因为这是将响应发送回客户端的方法。这使得它更容易通过一个新的版本控制器来发展API,而不会破坏现有的客户端。
作者的数据库集合
Node的Mongoose对象建模首先需要安装一个MongoDB数据库。我建议在您的本地开发设备上设置一个玩MongoDB.最低安装只需要两个可执行文件,你可以在大约50 MB的时间内启动并运行服务器。这是MongoDB的真正力量,因为一个完整的数据库可以在非常便宜的硬件上运行,比如树莓派,并且这可以水平扩展到尽可能多的盒子。该数据库还支持混合模型,其中服务器可以在云和on-prem上运行。所以,不要找借口!
在模型
文件夹,打开index.js
使用实例建立数据库连接。
常量配置=需要(“. . /配置”)常量猫鼬=需要(“猫鼬”)模块.出口={连接:异步函数(){等待猫鼬.连接(配置.DB_HOST+' / '+配置.DB_NAME,配置.DB_OPTS)},连接:猫鼬.连接,作者:需要(”。/作者)}
注意作者
集合定义在Author.js
在同一个文件夹中:
常量猫鼬=需要(“猫鼬”)常量authorSchema=新猫鼬.模式({的名字:字符串,电子邮件:字符串,主题:[字符串],createdAt:日期})如果(!authorSchema.选项.toObject)authorSchema.选项.toObject={}authorSchema.选项.toObject.变换=函数(医生,受潮湿腐烂){删除受潮湿腐烂._id删除受潮湿腐烂.__v如果(受潮湿腐烂.主题& &受潮湿腐烂.主题.长度= = =0)删除受潮湿腐烂.主题返回受潮湿腐烂}模块.出口=猫鼬.模型(“作者”,authorSchema)
请记住,Mongoose模式并不反映与Joi验证相同的需求。这增加了数据的灵活性,以支持多个版本,以防有人需要跨多个端点的向后兼容性。
的toObject
变换清除JSON输出,这样Joi验证器就不会抛出异常。如果有任何额外的字段,比如_id
,在Mongoose文档中,服务器发送一个500(内部服务器错误)响应。可选字段主题
当它是一个空数组时,会被破坏,因为GET必须匹配PUT响应。
最后,设置数据库配置配置/ dev.json
:
{“APP_PORT”:3000,“APP_HOST”:“127.0.0.1”,“DB_HOST”:“mongodb: / / 127.0.0.1:27017”,“DB_NAME”:“hapiAuthor”,“DB_OPTS”:{“useNewUrlParser”:真正的,“useUnifiedTopology”:真正的,“poolSize”:1}}
行为驱动开发
在为控制器中的每个方法充实端点之前,我喜欢从编写单元测试开始。这有助于我对手头的问题进行概念化,从而获得最佳的代码。我将使用红色/绿色,但跳过重构,并将其作为练习留给您,以免赘述这一点。
我将选择Hapi的实验室实用程序和他们的BDD断言库来测试我写的代码:
npmI @hapi/lab @hapi/code—save-dev
在测试/ Author.js
将这个基本的脚手架添加到测试代码中。我将选择行为驱动开发(BDD)风格,使其更加流畅:
常量实验室=需要(“@hapi /实验室”)常量{预计}=需要(“@hapi /代码”)常量{后,之前,描述,它}=出口.实验室=实验室.脚本()常量{初始化}=需要(“. . /服务器”)常量{连接}=需要(“. . /模式”)常量id=“5 ff8ea833609e90fc87fee52”常量有效载荷={的名字:“C R”,电子邮件:“xyz@abc.net”,createdAt:2021 - 01 - 08 - t06:00:00.000z}描述(' / v1 /作者的,()= >{让服务器之前(异步()= >{服务器=等待初始化()})后(异步()= >{等待服务器.停止()等待连接.关闭()})})
当您构建更多的模型和端点时,我建议在每个测试文件中重复相同的脚手架代码。单元测试不是DRY(“不要重复你自己”),启动/停止服务器和数据库连接是完全没问题的。MongoDB连接和Hapi服务器可以处理这个问题,同时保持测试流畅。
除了一个小问题外,测试几乎已经准备好运行了AuthorV1Controller1
,因为它是空的。打开控制器/ AuthorV1Controller.js
再加上这个:
出口.细节=()= >{}出口.插入=()= >{}出口.删除=()= >{}
测试通过npm t
在终点站。一定要把它放进去package.json
:
“脚本”:{“测试”:“实验室”},
继续,启动单元测试。应该还没有什么失败。要使单元测试失败,请将此添加到其中描述()
:
它(“PUT响应201”,异步()= >{常量{statusCode}=等待服务器.注入({方法:“把”,url:`/ v1 /作者/$ {id}`,有效载荷:{...有效载荷}})预计(statusCode).来.平等的(201)})它(“PUT响应200”,异步()= >{常量{statusCode}=等待服务器.注入({方法:“把”,url:`/ v1 /作者/$ {id}`,有效载荷:{...有效载荷,主题:[JavaScript的,MongoDB的]}})预计(statusCode).来.平等的(200)})它('GET响应200',异步()= >{常量{statusCode}=等待服务器.注入({方法:“得到”,url:`/ v1 /作者/$ {id}`})预计(statusCode).来.平等的(200)})它('DELETE响应204',异步()= >{常量{statusCode}=等待服务器.注入({方法:“删除”,url:`/ v1 /作者/$ {id}`})预计(statusCode).来.平等的(204)})
要开始通过单元测试,请把这个放在里面控制器/ AuthorV1Controller.js
:
常量db=需要(“. . /模式”)出口.细节=异步(请求,h)= >{常量作者=等待db.作者.findById(请求.参数个数.id).执行()请求.日志([“实现”],`GET 200 /v1/authors$ {作者}`)返回h.响应(作者.toObject())}出口.插入=异步(请求,h)= >{常量作者=等待db.作者.findById(请求.参数个数.id).执行()如果(!作者){常量newAuthor=新db.作者(请求.有效载荷)newAuthor._id=请求.参数个数.id等待newAuthor.保存()请求.日志([“实现”],`PUT 201 /v1/authors$ {newAuthor}`)返回h.响应(newAuthor.toObject()).创建(`/ v1 /作者/$ {请求.参数个数.id}`)}作者.的名字=请求.有效载荷.的名字作者.电子邮件=请求.有效载荷.电子邮件作者.主题=请求.有效载荷.主题请求.日志([“实现”],`PUT 200 /v1/authors$ {作者}`)等待作者.保存()返回h.响应(作者.toObject())}出口.删除=异步(请求,h)= >{等待db.作者.findByIdAndDelete(请求.参数个数.id)请求.日志([“实现”],`删除204 /v1/authors$ {请求.参数个数.id}`)返回h.响应().代码(204)}
这里有几件事需要注意。的exec ()
方法是物化查询并返回Mongoose文档的方法。因为这个文档有Hapi服务器不关心的额外字段,所以应用toObject
在调用之前反应()
.API的默认状态代码是200,但是可以通过代码()
或创建()
.
在红色/绿色/重构测试驱动开发中,我只编写了通过测试所需的最少的代码。我将把编写更多的单元测试和用例留给您。例如,当目标资源没有作者时,GET和DELETE应该返回404 (Not Found)。
Hapi支持其他特性,比如在请求
对象。作为默认值,实现
标签在服务器运行时将调试日志发送到控制台,这也适用于单元测试。这是一种很好的干净的方式,可以查看请求通过请求管道时发生了什么。
测试
最后,在启动主服务器之前,把这个放进去index.js
:
常量{开始}=需要(“。/服务器”)开始()
一个npm开始
应该让你在Hapi中得到一个运行和工作的REST API。现在我将使用Hoppscotch向所有端点发出请求。你所要做的就是点击下面的链接来测试你的API。请务必从上到下点击链接:
或者,在cURL中也可以做同样的事情:
旋度-i -X PUT -H“application / json内容类型:- d“{\”的名字\”:\”C R\”,\”电子邮件\”:\”xyz@abc.net\”,\”createdAt\”:\”2021 - 01 - 08 - t06:00:00.000z\”}”http://localhost:3000/v1/authors/5ff8ea833609e90fc87fee52201创建{“名称”:“C R”,“电子邮件”:“xyz@abc.net”,“createdAt”:“2021 - 01 - 08 - t06:00:00.000z”}旋度-i -X PUT -H“application / json内容类型:- d“{\”的名字\”:\”C R\”,\”电子邮件\”:\”xyz@abc.net\”,\”createdAt\”:\”2021 - 01 - 08 - t06:00:00.000z\”,\”主题\”:【\”JavaScript\”,\”MongoDB\”]}”http://localhost:3000/v1/authors/5ff8ea833609e90fc87fee52200好吧{“主题”:[“JavaScript”,“MongoDB”],“名称”:“C R”,“电子邮件”:“xyz@abc.net”,“createdAt”:“2021 - 01 - 08 - t06:00:00.000z”}旋度-我- h“application / json内容类型:http://localhost:3000/v1/authors/5ff8ea833609e90fc87fee52200好吧{“主题”:[“JavaScript”,“MongoDB”],“名称”:“C R”,“电子邮件”:“xyz@abc.net”,“createdAt”:“2021 - 01 - 08 - t06:00:00.000z”}旋度-i -X DELETE -H“application / json内容类型:http://localhost:3000/v1/authors/5ff8ea833609e90fc87fee52204没有内容
在Jamstack中,JavaScript客户端可以通过fetch ()
.REST API的好处在于它根本不需要是浏览器,因为任何支持HTTP的客户端都可以。这对于多个客户端可以通过HTTP调用API的分布式系统来说是完美的。API可以保持独立,具有自己的部署计划,并允许自由发展。
结论
JamStack有一种通过版本控制的端点和模型验证来解耦软件模块的好方法。Hapi服务器支持这一点和其他细节,如类型声明,使您的工作更愉快。