编写 Node.js Rest API 的 10 个最佳实践
zvbv3375
8年前
<p>Node.js 除了用来编写 WEB 应用之外,还可以用来编写 API 服务,我们在本文中会介绍编写 Node.js Rest API 的最佳实践,包括如何命名路由、进行认证和测试等话题,内容摘要如下:</p> <ol> <li> <p>正确使用 HTTP Method 和路由</p> </li> <li> <p>正确的使用 HTTP 状态码</p> </li> <li> <p>使用 HTTP Header 来发送元数据</p> </li> <li> <p>为 REST API 挑选合适的框架</p> </li> <li> <p>要对 API 进行黑盒测试</p> </li> <li> <p>使用基于 JWT 的无状态的认证机制</p> </li> <li> <p>学会使用条件请求机制</p> </li> <li> <p>拥抱接口调用频率限制(Rate-Limiting)</p> </li> <li> <p>编写良好的 API 文档</p> </li> <li> <p>对 API 技术演化保持关注</p> </li> </ol> <h2>1. 正确使用 HTTP Method 和路由</h2> <p>试想你正要构建一个 API 用来创建、更新、获取、删除用户,对于这些操作, HTTP 规范里面已经有了现成的操作: POST 、 PUT 、 GET 、 DELETE ,建议直接使用他们来描述接口的行为。</p> <p>至于路由的命名,应该使用名词或名词性短语来作为资源标识符,比如上文提到的用户管理的例子,路由就应该长这样:</p> <ul> <li> <p>POST /users 或者 PUT /users/:id 用来创建新用户;</p> </li> <li> <p>GET /users 用来获取用户列表;</p> </li> <li> <p>GET /users/:id 用来获取单个用户;</p> </li> <li> <p>PATCH /users/:id 用来更新用户信息;</p> </li> <li> <p>DELETE /users/:id 用来删除用户;</p> </li> </ul> <h2>2. 正确的使用 HTTP 状态码</h2> <p>如果服务器端在请求处理的过程中出错了,你必须设置正确的响应状态码,具体如下:</p> <ul> <li> <p>2xx ,表示一切正常;</p> </li> <li> <p>3xx ,表示资源位置已经更改;</p> </li> <li> <p>4xx ,表示因为客户端错误而导致请求无法被处理,比如参数校验没通过;</p> </li> <li> <p>5xx ,表示因为服务器错误导致请求无法被处理,比如服务端抛了异常;</p> </li> </ul> <p>如果你使用 express ,设置状态码非常简单: res.status(500).send({ error: 'Internal server error happend' }) ,如果使用了 restify ,也是类似的: res.status(201) 。</p> <h2>3. 使用 HTTP Header 来发送元数据</h2> <p>如果想要发送关于响应体数据的元数据,可以使用 Header , Header 可以包含的常见元数据包括如下几类:</p> <ul> <li> <p>分页信息;</p> </li> <li> <p>频率限制信息;</p> </li> <li> <p>认证信息;</p> </li> </ul> <p>如果你需要在 Header 中发送自定义的元数据,最好的做法是在 Header 名称前面加 X ,例如,需要发送 CSRF Token 的时候,实际的 Header 应该命名为: X-CSRF-Token ,然而,这种 Header 在 RFC 6648 中已经被废弃了。API 在设置自定义 Header 的时候还要尽可能避免命名冲突,比如为了达到这个目的 OpenStack 为所有 API 的自定义 Header 都加上了 OpenStack 的前缀:</p> <pre> <code class="language-javascript">OpenStack-Identity-Account-ID OpenStack-Networking-Host-Name OpenStack-Object-Storage-Policy</code></pre> <p>需要注意的是,虽然 HTTP 规范中没有规定 Header 的大小,但是 Node.js 中 Header 的大小被限制在了 80KB 。官方原文如下:</p> <p>不要让 HTTP Header ,包括其中状态码那行的整体大小超过 HTTP_MAX_Header_SIZE,这样做的目的是为了防御基于 Header 的 DDOS 攻击。 </p> <h2>4. 为 REST API 挑选合适的框架</h2> <p>根据你的实际场景挑选合适的框架是非常重要的, Node.js 中的框架大致介绍如下:</p> <h3>Express、Koa、HAPI</h3> <p>Express 、 Koa 、 HAPI 主要是用来构建浏览器 WEB 应用,因为他们都支持服务端模板渲染,虽然这只是他们众多功能中的一个。如果你的应用需要提供用户界面,那么这三个就是不错的选择。</p> <h3>Restify</h3> <p>而 Restify 是专门用来创建符合 REST 规范的服务的,他诞生的目的就是帮你构建严格意义上的、可维护的 API 服务。 Restify 内置了所有请求处理函数的 DTrace 支持。并且已经被 npm 和 netflix 用来在生产环境提供重要的服务。</p> <h2>5. 要对 API 进行黑盒测试</h2> <p>测试 API 的最好办法是对他们进行黑盒测试,黑盒测试是一种不关心应用内部结构和工作原理的测试方法,测试时系统任何部分都不应该被 mock 。</p> <p>supertest 是可以用来对接口进行黑盒测试的模块之一,下面是基于测试框架 <a href="/misc/goto?guid=4958968603421901612" rel="nofollow,noindex">mocha</a> 编写的一个测试用例,该用例的目的是检查接口是否能返回单条的用户数据:</p> <pre> <code class="language-javascript">const request = require('supertest') describe('GET /user/:id', function() { it('returns a user', function() { // newer mocha versions accepts promises as well return request(app) .get('/user') .set('Accept', 'application/json') .expect(200, { id: '1', name: 'John Math' }, done); }); });</code></pre> <p>可能有人会问: API 服务所连接的数据库里面的数据是如何写进去的呢?</p> <p>通常来说,你写测试的时候,要尽可能不对系统状态做假设,然而在某些场景下,你需要准确的知道系统当前所处的状态以增加更多的断言来提高测试覆盖率。如果你有这种需求,你可以试用如下的方法对数据库进行预填充:</p> <ul> <li> <p>选择生产环境数据的子集来运行黑盒测试;</p> </li> <li> <p>运行黑盒测试之前把手工构造的数据填充到数据库中。</p> </li> </ul> <p>此外,有了黑盒测试并不意味着不需要单元测试,针对 API 的 单元测试 还是需要编写的。</p> <h2>6. 使用基于 JWT 的无状态的认证机制</h2> <p>因为 Rest API 必须是无状态的,因此认证机制也需要是无状态的,而基于 JWT(JSON Web Token) 的认证机制是无状态认证机制中的最佳解决方案。</p> <p>JWT 的认证机制包含三部分:</p> <ol> <li> <p>Header :包含 token 的类型和哈希算法;</p> </li> <li> <p>payload :包含声明信息;</p> </li> <li> <p>signature : JWT 实际上并不是对 payload 进行加密,只是对其做了签名;</p> </li> </ol> <p>为 API 添加基于 JWT 的认证机制也非常的简单,比如下面的代码:</p> <pre> <code class="language-javascript">const koa = require('koa'); const jwt = require('koa-jwt'); const app = koa(); app.use(jwt( secret: 'very-secret' })); // Protected middleware app.use(function*() // content of the token will be available on this.state.user this.body = { secret: '42' } });</code></pre> <p>有了如上的代码,你的 API 就有了 JWT 的保护。如果要访问这种被保护的接口,需要使用 Authorization Header 来提供 token ,比如:</p> <pre> <code class="language-javascript">curl --Header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" my-website.com</code></pre> <p>你可能注意到了, JWT 模块并不依赖任何数据存储层,这是因为 token 本身是可以单独被校验的, token 里面的 payload 甚至可以包含 token 的签名时间、有效期限。</p> <p>此外,你还需要确保,所有的 API 接口只能通过更安全的 HTTPS 链接来访问。</p> <h2>7. 学会使用条件请求机制</h2> <p>条件请求机制是基于不同的 Header 表现出不同的行为的机制,可以认为这些 Header 就是请求处理方式的先决条件,如果条件满足,请求处理方式就会有所不同。</p> <p>可以利用这些 Header 检测服务器上的资源版本是否匹配特定的资源版本,这些 Header 的取值可以是如下的内容:</p> <ul> <li> <p>资源的最后修改时间;</p> </li> <li> <p>资源的标签(随资源变化而变化);</p> </li> </ul> <p>具体来说:</p> <ul> <li> <p>Last-Modified :标识资源的最新修改时间;</p> </li> <li> <p>Etag :标识资源的标签;</p> </li> <li> <p>If-Modified-Since :结合 Last-Modified Header 使用;</p> </li> <li> <p>If-Non-Match :结合 Etag 使用;</p> </li> </ul> <p>下面来看一个实际的例子:</p> <p>客户端不知道 doc 资源的任何版本,所以请求时即不能提供 If-Modified-Since ,也不能提供 If-Non-Match 两个 Header,然后服务端在响应中会增加 Etag 和 Last-Modified 两个 Header 。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/3068813ef9fa37990f7717efc8edcb77.png"></p> <p>接下来,客户端再次请求相同的资源的时候,就可以带上 If-Modified-Since 和 If-Non-Match 这两个 Header 了,然后如果服务器端会检查资源是否修改,如果没有修改,直接返回 304 - Not Modified 状态码,而不重复发送资源的内容。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/9e2b7c8a57732a0fb9564dcac38e3c82.png"></p> <h2>8. 拥抱接口调用频率限制(Rate-Limiting)</h2> <p>频率限制是用来控制调用方有对接口发起请求的次数,为了让你的 API 用户知道他们还剩下多少余额,可以设置下面的 Header :</p> <ul> <li> <p>X-Rate-Limit-Limit:特定时间段内允许的最多请求次数;</p> </li> <li> <p>X-Rate-Limit-Remaining:特定时间段内剩余的请求次数;</p> </li> <li> <p>X-Rate-Limit-Reset:什么时候请求频率限制次数会重置;</p> </li> </ul> <p>大多数的 WEB 框架都支持上面这些 Header ,如果内置不支持,也可以找到插件来支持,比如,如果你使用了 koa ,可以使用 koa-rate-limit 。</p> <p>需要注意的是,不同的 API 服务提供商频率限制的时间窗差异会很大,比如 GitHub 是 60 分钟,而 推ter 是 15 分钟。</p> <h2>9. 编写良好的 API 文档</h2> <p>编写 API 的目的当然是让别人使用并受益,提供良好的接口文档至关重要。下面这两个开源项目可以帮你创建 API 文档:</p> <ul> <li> <p><a href="/misc/goto?guid=4958966062069167414" rel="nofollow,noindex">API Blueprint</a></p> </li> <li> <p><a href="/misc/goto?guid=4958966062191945523" rel="nofollow,noindex">Swagger</a></p> </li> </ul> <p>如果你愿意使用第三方文档服务商,可以考虑 Apiary 。</p> <h2>10. 对 API 技术演化保持关注</h2> <p>过去几年中, API 技术方案领域出现了两种新的查询语言,分别是 非死book 的 GraphQL 和 Netflix 的 Falcor ,为什么需要他们呢?</p> <p>试想这种 API 接口请求: /org/1/space/2/docs/1/collaborators?include=email&page=1&limit=10 ,类似的情况会让 API 很快失控,如果你希望所有接口能返回类似的响应格式,那么 GraphQL 和 Falcor 就能帮你解决这个问题。</p> <p>关于 GraphQL:</p> <p>GraphQL 是一种用于 API 的查询语言,也是一种基于现有数据处理数据查询的运行时。GraphQL 为您的 API 中的数据提供了一个完整和可理解的描述,使用户能够准确地询问他们需要什么,使得随着时间推移的 API 演化更容易,GraphQL 还有强大的开发工具支持。 </p> <p>关于 Falcor:</p> <p>Falcor 是支撑着 Netflix UI 的创新数据平台。Falcor 允许你将所有后端数据建模为 Node.js 服务商的单个虚拟 JSON 对象。在客户端可以使用熟悉的 JavaScript 操作、处理远程JSON对象。如果你知道你的数据,你就知道你的 API 长啥样。 </p> <h2>能带来灵感的优秀 API 设计</h2> <p>如果你正在开发 Rest API 或者准备改进老版本的 API ,这里收集了几个在线上提供服务、设计优秀并且非常直接借鉴的 API :</p> <ul> <li> <p><a href="/misc/goto?guid=4958863874080865968" rel="nofollow,noindex">GitHub API</a></p> </li> <li> <p><a href="/misc/goto?guid=4959739807392168815" rel="nofollow,noindex">Twilio API</a></p> </li> <li> <p><a href="/misc/goto?guid=4959650958550912852" rel="nofollow,noindex">Stripe API</a></p> </li> <li> <p><a href="/misc/goto?guid=4959739807504369092" rel="nofollow,noindex">Digital Ocean API</a></p> </li> </ul> <p> </p> <p> </p> <p>来自:https://segmentfault.com/a/1190000008537712</p> <p> </p>