MENU

Express 实战(六):构建 API 接口

Cover

在介绍了那么多 Express 核心概念之后,接下来的文章将会把注意力放在如何构建一个真实的应用上。这里我们先从构建应用 API 接口开始。从某种程度上来说几乎所有的软件应用其背后都是由一组强大的 API 驱动。

其实 API 就是一种代码之间交互的一种方式,它既可以是在程序内部也可以是通过网络的跨机器进行。例如,Express 中的 app.useapp.get 就属于在内部使用 API 。而通过 HTTP 或者 FTP 等协议发送 JSON、XML 数据的方式则属于后者。对于后一种方式需要注意的是,API 的提供者和使用者必须对数据格式做出约定。在本文示例中,我们将会讨论如何使用 Express 构建后一类型的 API 接口,同时所有 HTTP 接口返回的数据格式都将使用 JSON。

另外,本章还会讨论如何设计一个优雅的 API 用于提升使用者的体验和效率,让 API 的含义一目了然而不用去阅读又臭又长的说明文档。就像“好代码”与“坏代码”一样,API 是否优雅其实更多的取决于实际情形。盲目遵循 API 设计的最佳实践有时会显得很迂腐,因为它有可能与使用者的期望不一致。

接下来的内容包括:

  • 什么是 API 。

  • Express 中构建 API 的基础内容。

  • HTTP 方法与应用逻辑的关联。

  • 多版本 API 的实现和管理。

  • HTTP 状态码的正确使用。

简单的 JSON 格式 API 示例

首先,我们需要明确该示例的功能以及 API 的使用方式,后面再写代码。

假设,现在程序需要在接受到 America/Los_AngelesEurope/London 等代表时区的字符串后,返回该时区的当前时间信息(例如:2015-04-07T20:09:58-07:00 )。该返回信息与现实中易懂的时间格式是不一样的,因为它是为计算机设计的。

通过类似下面格式的 URL 的 HTTP 请求来调用应用 API:

/timezone?tz=America+Los_Angeles

而服务端 API 返回的 JSON 的数据格式,如下:

{
    "time": "2015-06-09T16:20:00+01:00",
    "zone": "America/Los_Angeles"
}

只要能调用 API 并对 JSON 数据进行解析,你就可以在任意平台构建任意应用程序。如下图,你可以通过 AJAX 请求该 API 实现一个展示时区信息的单页应用。

06_01

你也可以利用该接口实现下图所示的移动应用。

06_02

你甚至可以利用该 API 实现下图一样的终端命令行工具:在终端中打印服务端 API 接口返回的数据。

06_03

像前一章的天气应用一样,我们可以利用这些 API 返回的冰冷数据构建更具表达力的 UI 。

Express 驱动的 JSON API 服务

了解 API 概念之后,下面我们就动手实现一个 Express 驱动的 API 服务。实现的原理非常简单:通过中间件和内置函数解析网络请求并将 JSON 数据和 HTTP 状态码封装到响应对象并返回给客户端。

从技术角度上说,API 服务除了使用 JSON 格式外,你还可以是使用 XML 或者纯文本。但是 Express 和 JavaScript 对 JSON 的支持是最好的,同时它也是当前最流行的格式,所以后面会一直使用 JSON 作为默认数据格式。

下面我们编写一个为多平台提供随机数生成的服务,该 API 将拥有如下特性:

  • 在请求 API 时必须附带随机数最小值和最大值。

  • 解析请求获取随机数范围并将生产的结果以 JSON 格式返回。

你可能认为这里完全可以使用纯文本来替换 JSON 格式。但是发送 JSON 数据是开发者的必备技能,而且 JSON 格式极易拓展。

该工程的构建步骤如下:

  1. 新建 package.json

  2. 创建工程主入口文件 app.js

  3. app.js 中创建应用和路由中间件。

首先,在新建的 package.json 中,复制下面的内容并按照依赖项:

{
    "name": "random-number-api",
    "private": true,
    "scripts": {
        "start": "node app"
    },
    "dependencies": {
        "express": "^5.0.0" 
    }
}

接下来,将下面的代码复制到入口文件 app.js 中:

var express = require("express");
var app = express();

app.get("/random/:min/:max", function(req, res) {
    var min = parseInt(req.params.min);
    var max = parseInt(req.params.max);
    if (isNaN(min) || isNaN(max)) {
        res.status(400);
        res.json({ error: "Bad request." });
        return;
    }
   
    var result = Math.round((Math.random() * (max - min)) + min);
    res.json({ result: result });
});

app.listen(3000, function() {
    console.log("App started on port 3000");
});

现在启动应用并访问 http://localhost:3000/random/10/100 的话,你将看到一个附带 10 ~ 100 范围内随机数的 JSON 数据。

06_04

接下来,我们来分析上面的代码。

与之前一样,前两行代码引入了 Express 并创建了一个 Express 应用实例。

然后,我们创建了一个路由中间件用于处理类似 /random/10/100 这样的 API 请求。当然,这里还存在一些 bug ,例如,没有过滤掉 /random/foo/bar 请求。所以,在调用 API 的时候请确保使用的参数是整型变量。

在然后,我们使用内置的 parseInt 解析范围参数,而该函数的返回值只可能是整形数字或者 NaN。如果传入的参数有一个为 NaN 的话就会给客户端返回一个错误信息。下面这部分代码对于整个程序来说是非常重要的:

if (isNaN(min) || isNaN(max)) {
  res.status(400);
  res.json({ error: "Bad request." });
  return;
}

如果上面的参数检查的结果是最少有一个为 NaN ,程序就会进行如下处理:

  1. 设置 HTTP 状态码为 400。常见的 404 错误就是它的一个具体变种,表示的含义是:用户请求的出现了问题。

  2. 发送包含错误信息的 JSON 数据。

  3. 结束请求处理并跳出中间件执行。

在代码的最后,我们会在合法的参数返回内生成随机数并将结果返回给客户端。

虽然示例很简单,但是它已经包含了使用 Express 构建 API 的基本流程:解析请求,设置 HTTP 状态码,返回响应数据。你可以在这个基础之上构建更为复杂优雅的 API 。

CURD 操作 API

CURD 是对程序中 Create、Read、Update、Delete 四种业务动作的一个简称。

大多数的应用都会涉及到 CURD 操作。例如,对于一个图片分享应用来说,其中涉及图片的所有操作就是典型的 CRUD:

  • 用户上传照片的行为对应就是 create 操作。

  • 用户浏览照片的行为就是 read 操作。

  • 用户更新照片的行为就是 update 操作。

  • 用户删除照片的行为就是 delete 操作。

无论是分享照片的社交应用还是文件存储服务,你生活中的使用的很多服务中都使用了这种模式。不过在开始讨论构建 CRUD 功能的 API 之前,我们先来看看被称为 HTTP 方法的内容。

HTTP 方法

HTTP 的规范中是这样定义其方法的:

HTTP 方法明确了对请求 URI 所标识资源进行的操作,而且方法是区分大小写的。

一个更易理解的解释是:客户端在发送 HTTP 请求时需要指定一个 HTTP 方法,然后服务端回依据不同的 HTTP 方法做出不同的响应。虽然,可用的 HTTP 方法有很多,但是常用的其实并不多。其中在 Web 应用中常用是下面 4 个:

  1. GET 是最常用的一个 HTTP 方法,它表示请求服务端资源。例如,加载网站首页、请求图片资源都使用的是 GET。虽然服务端的响应可能不同,但是GET 请求并不会改变服务器的资源。例如,对某图片资源的一次或者多次请求并不会导致图片本身出现任何差别。

  2. POST 是另一个常用的 HTTP 方法。例如,创建新博客、上传照片、注册用户、清空购物车等业务都是使用 POST 。与 GET 不同的是:每次 POST 请求都会导致服务端发生修改。

  3. PUT 方法用于对已有记录的修改,所有我觉得它应该被称为 "UPDATE" 更为合适。例如,修改博客标题、修改用户昵称等操作都是 PUT 操作。另外,PUT 还具备 POST 的功能:就是当要修改的记录不存在时可以进行新建操作(非必需)。其次 PUT 还具有 GET 方法的特点:对同一 URL 的一次或多次 PUT 请求后的结果是一致的。

  4. DELETE 方法用于记录删除。例如,删除用户文章、删除网络照片。另外,与 PUT 一样同一删除请求无论是执行一次还是多次最终结果是一致的。

虽然 HTTP 还有很多其他的方法,但是它们在现实开发过程中并不常见。理论上你甚至可以只使用 GET 和 POST 请求完成所有业务,但是这是错误实践毕竟它违反了 HTTP 规范也会给开发者造成困惑。另外,很多浏览器也是根据 HTTP 方法来明确所执行的操作类型。所以,即使并没有强制你也应该参照该规范来约束自己的行为。

前面你已经见过 Express 中对部分方法的处理,不过下面的代码将一次涵盖上面所有的四个方法:

var express = express("express");
var app = express();

app.get("/", function(req, res) {
  res.send("you just sent a GET request, friend");
});

app.post("/", function(req, res) {
  res.send("a POST request? nice");
});

app.put("/", function(req, res) {
  res.send("i don't see a lot of PUT requests anymore");
});

app.delete("/", function(req, res) {
  res.send("oh my, a DELETE??");
});

app.listen(3000, function() {
  console.log("App is listening on port 3000");
});

将代码复制到入口文件 app.js 中并启动服务,然后你就可以使用 cURL 命令测试不同的 HTTP 方法了。默认情况下 cURL 使用 GET 发送请求,但是你可以使用 -X 选项来指定其他的方法。例如,curl -X PUT http://localhost:3000

06_05

通过 HTTP 方法构建 CRUD 接口

回想以下之前的照片分享应用,下面是其中可能的 CRUD 操作:

  1. 用户上传图片,此为 Create

  2. 用户浏览图片,此为 Read

  3. 用户更新图片备注等信息,此为 Update

  4. 用户从站点删除图片,此为 Delete

不难看出 CRUD 操作与之前四种 HTTP 方法存在对应关系:

  • Create = POST

  • Read = GET

  • Update = PUT

  • Delete = DELETE

因此通过这四个 HTTP 方法我们可以很好的实现最常见 CRUD 风格的 web 应用程序。

实际上对于更新和创建动作与 HTTP 方法的对应关系,一些人有着自己的看法。它们认为 PUT 更应该对应创建动作而非 POST。另外,新的 PATCH 方法则对应更新操作。虽然本文将会使用上面那种更规范的对应关系,但是你完全可以按照自己的意愿选择。

API 版本控制

为了应对未来可能的 API 更新,对 API 进行版本控制是一件非常高效的方法。例如,前面获取指定时区当前时间的 API 在推出后就被很多的厂商和开发者使用。但是,几年几后由于某些原因必须对该 API 进行更新而与此同时你又不能影响之前的使用者。此时,我们就可以通过添加新版本来解决这个问题。其中原有的 API 请求可以通过:

/v1/timezone

而新版本 API 请求则可以使用:

/v2/timezone

这样不仅在进行 API 更新时防止了代码的破坏性更改。而且接口使用者也有了更灵活的选择,他们可以在必要的时候进行 API 切换。

在 Express 中可以使用 Router 中间件来实现 API 版本管理。拷贝下面代码到文件 app1.js 中,并讲其作为第一个版本 API 的实现:

var express = require("express");
var api = express.Router();

api.get("/timezone", function(req, res) {
    res.send("Sample response for /timezone");
});
api.get("/all_timezones", function(req, res) {
    res.send("Sample response for /all_timezones");
});

module.exports = api;

请注意,上面的中间件代码在处理的 URL 并没有包含 /v1 。下面在入口文件中引入这个 Router 中间件并进行路由映射。

var express = require("express");
var apiVersion1 = require("./api1.js");
var app = express();
app.use("/v1", apiVersion1);
app.listen(3000, function() {
    console.log("App started on port 3000");
});

然后,你将最新版本的 API 实现放在 api2.js 文件中:

var express = require("express");
var api = express.Router();

api.get("/timezone", function(req, res) {
    res.send("API 2: super cool new response for /timezone");
});
module.exports = api;

最后,通过 Router 将这两个版本的 API 同时添加到主入口中:

var express = require("express");

var apiVersion1 = require("./api1.js");
var apiVersion2 = require("./api2.js");
var app = express();

app.use("/v1", apiVersion1);
app.use("/v2", apiVersion2);
app.listen(3000, function() {
    console.log("App started on port 3000");
});

你可以通过浏览器验证这些版本化后的 API 是否正确工作,另外你也可以使用 cURL 命令进行测试。

06_07

就像前面章节介绍的那样,Router 可以让你将不同的路由存放在不同文件中进行管理。而版本化 API 就是最典型的应用实例。

设置 HTTP 状态码

每一个 HTTP 响应都应该附带一个 HTTP 状态码,其中最有名的就是 404 Not Found

虽然 404 是最出名的,但是 200 状态码确是最常见的。与 404 不同的是,虽然当网页成功加载或 JSON 数据成功返回后都会包含状态码 200,但它并不会被展示出来。

当然,除了 404 和 200 之外,HTTP 中还定义了很多其他的状态码,包括 100、200、300、400 以及 500 系列。需要注意的是并不是每个系列中所有 100 个数字都有明确定义,例如,100 系列只有 100,101,102 三个有效码,紧跟其后就是 200 。

每个状态码系列其实都有特定的含义和主题,总结就是:

1xx: 成功接收到请求。
2xx: 成功
3xx: 重定向
4xx: 客户端错误
5xx: 服务端错误

规范中只定义的大约 60 个状态码。你可以在此基础上拓展自己的状态码,但是通常并不会这么做。因为优秀的 API 的首要设计原则就是确保不会对使用者造成任何歧义,所以应该最大程度遵循官方规范的指导。后面我们会对上面的每个区间的状态码进行讲解,但是在此之前先来看看如何在 Express 中设置状态码。

少部分应用还在使用 HTTP 1.0 版本的协议,而大部分以及切换到了 1.1 版本。作为下一个版本的 HTTP 2.0 标准现在也逐渐在推广过程中。幸运的是,2.0 版本的协议大部分更新都在底层所以切换时并不会涉及太大的工作量。另外,2.0 版本还新增了一个 421 的状态码。

设置 HTTP 状态码

默认情况下,HTTP 状态码是 200。如果用户访问的 URL 对应资源不存在的话,Express 会发送 404 错误。如果访问的服务器出现问题的话,Express 就会发送 500 错误。

但是这些都是 Express 的默认行为,某些情形下可能会需要自行设置状态码。为此,Express 的 response 对象提供了一个 status 方法,你需要在调用是传入对应状态码就能完成设置。

// ...
res.status(404);
// ...

该方法可以进行链式调用,所以你可以紧跟其后使用 json 设置返回的数据。

res.status(404).json({ error: "Resource not found!" });
// 它等价于:
res.status(404);
res.json({ error: "Resource not found!" });

虽然 Express 对原生 Node 的 response 对象进行了拓展,并且在使用 Express 时也应遵循 Express 风格,但是你依旧可以使用原生方法来完成设置。

res.statusCode = 404;

100 区间

100 区间的官方状态码只有两个:100(继续) 和 101 (切换协议),而且它们很少会被用到。如果你必须处理的话,可以去官网或者维基上查看。

200 区间

200 区间状态码表示请求成功。虽然该区间状态码不少,但是常用的也就下面 4 个:

  • 200:作为最常见的状态码,它也被称为 "OK"。这意味着请求和响应都正确执行期间并没有出现任何错误或者重定向操作。

  • 201:与 200 十分类似,但是使用情形略有不同。它通常用于 POST 或者 PUT 请求成功创建记录后。例如,创建博文、上传图片等操作成功后就会发送 201。

  • 202:202 是 201 的一个变种。因为,资源的创建大多是异步进行的,而这些操作也是费时的。所以,你可以在此时给客户端响应 202 。它表示已经成功接收数据正在等待创建。

  • 204:它表示用户删除请求所对应的资源并不存在已经被删除过了。

300区间

同样,在 300 区间,我们只介绍其中常用的三个,并且它们全都涉及重定向。

  • 301:它表示所访问资源位置已经发生修改,请访问最新的 URL 。通常它还会附带一个 Location 的头部信息指明重定向的位置。

  • 303:它表示请求的资源已经创建完成,现在你就会被重定位到一个新页面。

  • 307:与 301 类似都是提示当前 URL 不存在。不过区别是,301 的重定向是永久的而 307 可能重定向的只是一个临时性 URL 。

400 区间

400 区间的状态码是最多的,而它通常都是表示由于客户端的错误导致请求失败。

  • 401 和 403:这两个状态码分别表示“未授权”和“禁止”。字面上看两者很类似,但是前者可能表示用户未登录而后者则可能是用户登录了但是权限不够。

  • 404:它表示用户 URL 请求的资源并不存在。

至于该区间其他状态码,读者可以去维基上自行查看,这里就不一一介绍了。另外,当你不确定应该使用哪种客户端错误状态码时,你可以直接使用 400 。

500 区间

作为 HTTP 规范里的最后一个区间,500 区间状态码表示的是服务内部出现错误。例如,请求过载或者数据库连接中断。另外,理论上该区间的错误只能有服务内部自己触发。最后,为了防止黑客窥探太多内部信息,你可以对所有的内部错误仅仅返回一个抽象的“内部服务器错误”这样的信息。

总结

本章包含的内容有:

  • 使用 Express 构建 API 服务。

  • HTTP 方法以及与 CRUD 操作之间的关系。

  • 如果对 API 进行版本控制,提示服务的兼容性和稳定性。

  • HTTP 状态码的使用和其意义。