如何用Hapi和TypeScript为Jamstack构建一个Rest API

114 阅读9分钟

Jamstack有一个很好的方法来分离前端和后端,使整个解决方案不必在一个单一的单体中出货--而且都是在同一时间。当Jamstack与REST API配对时,客户端和API可以独立发展。这意味着前端和后端都不是紧密耦合的,改变一个并不一定意味着改变另一个。

在这篇文章中,我将从Jamstack的角度看一下REST API。我将展示如何在不破坏现有客户端的情况下发展API并遵守REST标准。我将选择Hapi作为构建API的工具,并选择Joi作为端点验证的工具。数据库持久层将在MongoDB中通过Mongoose来访问数据。测试驱动的开发将帮助我迭代变化,并提供一种快速获得反馈的方法,减少认知负担。最后,我们的目标是让你看到REST和Jamstack是如何在软件模块之间提供一个高内聚力和低耦合度的解决方案。这种类型的架构最适合于有很多微服务的分布式系统,每个微服务都在自己独立的领域内。我假设你对NPM、ES6+有一定的了解,并对API端点有基本的熟悉。

该API将与作者数据一起工作,包括姓名、电子邮件和一个可选的1:N(通过文档嵌入的一对一)关系的喜爱的主题。我将写一个GET、PUT(有一个upsert)和DELETE端点。为了测试这个API,任何支持fetch() 的客户端都可以,所以我将选择Hoppscotch和CURL。

我将保持这篇文章的阅读流程,就像一个教程,你可以从上到下跟随。对于那些想跳过代码的人来说,可以在GitHub上看到,供你欣赏。本教程假设Node的工作版本(最好是最新的LTS)和MongoDB已经安装

初始设置

要从头开始项目,请创建一个文件夹,并在cd

mkdir hapi-authors-rest-api
cd hapi-authors-rest-api

一旦进入项目文件夹,启动npm init ,并按照提示操作。这将在文件夹的根部创建一个package.json

每个Node项目都有依赖性。我需要Hapi、Joi和Mongoose来开始使用。

npm i @hapi/hapi joi mongoose --save-exact
  • @hapi/hapi。HTTP REST服务器框架
  • Joi:强大的对象模式验证器
  • Mongoose。MongoDB对象文档建模

检查package.json ,确保所有的依赖性和项目设置都已到位。然后,为这个项目添加一个入口。

"scripts": {
  "start": "node index.js"
},

带有版本管理的MVC文件夹结构

对于这个REST API,我将使用一个典型的MVC文件夹结构,包括控制器、路由和数据库模型。控制器将有一个类似于AuthorV1Controller 的版本,以允许API在模型有突破性变化时进行演进。Hapi将有一个server.jsindex.js ,以便通过测试驱动的开发使这个项目可以测试。test 文件夹将包含单元测试。

下面是整个文件夹的结构。

┳
┣━┓ config
┃ ┣━━ dev.json
┃ ┗━━ index.js
┣━┓ controllers
┃ ┗━━ AuthorV1Controller.js
┣━┓ model
┃ ┣━━ Author.js
┃ ┗━━ index.js
┣━┓ routes
┃ ┣━━ authors.js
┃ ┗━━ index.js
┣━┓ test
┃ ┗━━ Author.js
┣━━ index.js
┣━━ package.json
┗━━ server.js

现在,继续创建文件夹和每个文件夹中的各自文件。

mkdir config controllers model routes test
touch config/dev.json config/index.js controllers/AuthorV1Controller.js model/Author.js model/index.js routes/authors.js routes/index.js test/Authors.js index.js server.js

这就是每个文件夹的用途。

  • config:配置信息以插入Mongoose连接和Hapi服务器。
  • controllers终端:这些是处理Request/Response对象的Hapi处理程序。版本化允许每个版本号有多个端点--也就是/v1/authors/v2/authors ,等等。
  • model终端:连接到MongoDB数据库并定义Mongoose模式。
  • routes: 为REST纯粹主义者定义了带有Joi验证的端点。
  • test:通过Hapi的实验室工具进行单元测试。(稍后会有更多关于这个的内容)。

在一个真实的项目中,你可能会发现把普通的业务逻辑抽象成一个单独的文件夹,比如utils 。我建议用纯粹的功能代码创建一个AuthorUtil.js 模块,以使其可以跨终端重复使用,并易于单元测试。因为这个解决方案没有任何复杂的业务逻辑,我选择跳过这个文件夹。

添加更多的文件夹的一个问题是有更多的抽象层和更多的认知负荷,同时进行修改。在特别大的代码库中,很容易在混乱的误导层中迷失方向。有时,保持文件夹结构尽可能的简单和扁平会更好。

TypeScript

为了改善开发者的体验,我现在将添加TypeScript类型声明。因为Mongoose和Joi在运行时定义模型,所以在编译时添加类型检查器没有什么价值。在TypeScript中,可以将类型定义添加到一个普通的JavaScript项目中,并且仍然可以在代码编辑器中获得类型检查器的好处。像WebStorm或VS Code这样的工具会拾取类型定义,并允许程序员 "点 "到代码中。这种技术通常被称为IntelliSense,当IDE有可用的类型时,它就被启用。你所得到的是一种定义编程接口的好方法,这样开发者就可以在不看文档的情况下点入对象。当开发者点入错误的对象时,编辑器有时也会显示警告。

这就是VS Code中IntelliSense的样子。

VSCode IntelliSense

在WebStorm中,这被称为代码完成,但它本质上是一样的。请自由选择你喜欢的IDE来编写代码。我使用 Vim 和 WebStorm,但你可以选择不同的方式。

为了在这个项目中启用TypeScript类型声明,启动NPM并保存这些开发者依赖。

npm i @types/hapi @types/mongoose --save-dev

我建议将开发人员的依赖性与应用程序的依赖性分开。这样一来,组织中的其他开发者就能清楚地知道这些包是用来做什么的。当构建服务器拉下 repo 时,它也可以选择跳过项目在运行时不需要的包。

在所有开发人员的好处都到位后,现在是开始写代码的时候了。打开Hapiserver.js 文件,把主服务器放在那里。

const config = require('./config')
const routes = require('./routes')
const db = require('./model')
const Hapi = require('@hapi/hapi')

const server = Hapi.server({
  port: config.APP_PORT,
  host: config.APP_HOST,
  routes: {
    cors: true
  }
})

server.route(routes)

exports.init = async () => {
  await server.initialize()
  await db.connect()
  return server
}

exports.start = async () => {
  await server.start()
  await db.connect()
  console.log(`Server running at: ${server.info.uri}`)
  return server
}

process.on('unhandledRejection', (err) => {
  console.error(err)
  process.exit(1)
})

我已经通过设置cors ,启用了CORS,所以这个REST API可以与Hoppscotch一起工作。

为了保持简单,我将在这个项目中放弃分号。在这个项目中,跳过TypeScript的构建,打出那个额外的字符,多少有些自由。这遵循了Hapi的口号,因为无论如何,这都是为了开发者的幸福。

config/index.js ,一定要导出dev.json 信息。

module.exports = require('./dev')

为了充实配置服务器的内容,把这个放在dev.json

{
  "APP_PORT": 3000,
  "APP_HOST": "127.0.0.1"
}

REST验证

为了保持REST端点遵循HTTP标准,我将添加Joi验证。这些验证有助于将API与客户端解耦,因为它们强制执行资源完整性。对于Jamstack来说,这意味着客户端不再关心每个资源背后的实现细节。它可以自由地独立对待每个端点,因为验证将确保对资源的有效请求。遵循严格的HTTP标准使得客户端基于位于HTTP边界后面的目标资源而发展,这就强制了解耦。真的,目标是使用版本和验证来保持Jamstack中的一个干净的边界。

对于REST,主要目标是通过GET、PUT和DELETE方法来保持*空闲*。这些是安全的请求方法,因为对同一资源的后续请求不会有任何副作用。即使客户端未能建立连接,同样的预期效果也会重复出现。

我将选择跳过POST和PATCH,因为这些都不是安全的方法。这是为了简明扼要地说明问题,而不是因为这些方法以任何方式紧紧抓住了客户端。同样严格的HTTP标准可以适用于这些方法,只是它们不能保证空闲性。

routes/authors.js ,添加以下Joi验证。

const Joi = require('joi')

const authorV1Params = Joi.object({
  id: Joi.string().required()
})

const authorV1Schema = Joi.object({
  name: Joi.string().required(),
  email: Joi.string().email().required(),
  topics: Joi.array().items(Joi.string()), // optional
  createdAt: Joi.date().required()
})

请注意,对版本化模型的任何改变都可能需要一个新的版本,比如v2 。这保证了现有客户端的向后兼容性,并允许API独立发展。当有字段缺失时,需要的字段将以400(Bad Request)的响应来拒绝请求。

在参数和模式验证到位后,将实际的路由添加到该资源。

// routes/authors.js
const v1Endpoint = require('../controllers/AuthorV1Controller')

module.exports = [{
  method: 'GET',
  path: '/v1/authors/{id}',
  handler: v1Endpoint.details,
  options: {
    validate: {
      params: authorV1Params
    },
    response: {
      schema: authorV1Schema
    }
  }
}, {
  method: 'PUT',
  path: '/v1/authors/{id}',
  handler: v1Endpoint.upsert,
  options: {
    validate: {
      params: authorV1Params,
      payload: authorV1Schema
    },
    response: {
      schema: authorV1Schema
    }
  }
}, {
  method: 'DELETE',
  path: '/v1/authors/{id}',
  handler: v1Endpoint.delete,
  options: {
    validate: {
      params: authorV1Params
    }
  }
}]

为了使这些路由对server.js ,在routes/index.js 中添加这个。

module.exports = [
  ...require('./authors')
]

Joi验证放在路由数组的options 字段中。每个请求路径都需要一个字符串ID参数,与MongoDB中的ObjectId 。这个id 是版本化路由的一部分,因为它是客户端需要处理的目标资源。对于PUT,有一个有效载荷验证,与GET的响应相匹配。这是为了遵守REST标准,即PUT响应必须与随后的GET匹配

这就是标准中所说的。

一个给定表示的成功PUT将表明,在同一目标资源上的后续GET将导致在200(OK)响应中发送一个相等的表示。

这使得PUT不适合支持部分更新,因为随后的GET将不匹配PUT。对于Jamstack来说,坚持HTTP标准以确保客户端的可预测性和解耦性是很重要的。

AuthorV1Controller 通过v1Endpoint 中的方法处理程序来处理请求。每个版本都有一个控制器,这是一个好主意,因为这是将响应发回给客户端的。这使得通过一个新版本的控制器来发展API变得更加容易,而不会破坏现有的客户端。

继续阅读:在SitePoint 上用Hapi和TypeScript为Jamstack建立一个Rest API