10个关于Node.js REST API 的最佳实践

qlgz1425 8年前
   <p>在这篇文章里,我们将介绍Node.js REST API的最佳实践,包括关于路由命名,身份认证,黑盒测试,使用恰当的网络缓存等内容。</p>    <p>一个最流行的Node.js RESTful API监听工具 Trace ,通过Trace,我们帮助我们的用户寻找程序中的问题。我们的经验告诉我们,开发者开发REST API时有很多问题。</p>    <p>我希望这些用在 RisingStack 上的这些最佳实践能够帮助大家。</p>    <h3>1 使用HTTP方法和API路由</h3>    <p>想象一下,你写的Node.js RESTful API可以用于新建,更新,检索或者删除用户。 对于这些业务操作,HTTP已经有了成套的工具箱: POST , PUT , GET , PATCH 或 DELETE 。</p>    <p>作为最佳实践,你的 <strong>API路由应该使用名词作为资源标识符</strong> 。说到关于用户的资源,你可以像下面这样构建:</p>    <ul>     <li> <p>POST /user 或 PUT /user:/id 创建新用户</p> </li>     <li> <p>GET /user 查找一些用户</p> </li>     <li> <p>GET /user/:id 查找某个用户</p> </li>     <li> <p>PATCH /user/:id 编辑修改一个存在用户的信息</p> </li>     <li> <p>DELETE /user/:id 删除用户</p> </li>    </ul>    <h3>2 正确使用HTTP状态码</h3>    <p>当请求出现错误的时候,你必须返回相应的状态码:</p>    <ul>     <li> <p>2xx , 一切正常</p> </li>     <li> <p>3xx , 资源被移除</p> </li>     <li> <p>4xx , 客户端错误 <em>(比如请求一个不存在的资源)</em></p> </li>     <li> <p>5xx , API(服务端)错误 <em>(比如出现异常)</em></p> </li>    </ul>    <p>如果你使用Express,设置状态码非常简单 res.status(500).send({error: 'Internal server error happened'}) ,Restify框架的写法也差不多 res.status(201) 。</p>    <h3>3 使用HTTP头发送元数据(Metadata)</h3>    <p>在HTTP header上添加关于有效载荷(payload)的元数据(metadata),适用于以下场景:</p>    <ul>     <li> <p>分页</p> </li>     <li> <p>限制访问频率</p> </li>     <li> <p>身份验证</p> </li>    </ul>    <p>如果你需要在你的HTTP header里设置一些定制的元数据,最佳实践是在定制内容前加 X 。 举个例子,如果你在使用CSRF token,通用的命名方法(不是规范)是命名为 X-Csrf-Token 。当然通过 RFC 6648 的方式被弃用。创建的API时,要尽最大的可能避免命名冲突。比如,OpenStack在命名HTTP header时,以 OpenStack 开头:</p>    <pre>  <code class="language-javascript">OpenStack-Identity-Account-ID    OpenStack-Networking-Host-Name    OpenStack-Object-Storage-Policy</code></pre>    <p>however, Node.js (as of writing this article) imposes an 80KB size limit on the headers object for practical reasons.</p>    <p>注意,HTTP规范并没有对HTTP header大小进行限制;尽管如此,出于对应用场景的考虑,作者希望对HTTP header进行80KB的大小限制。</p>    <p>" 不要允许设置任意大小的 HTTP header (包括状态行) ,不要超过 HTTP_MAX_HEADER_SIZE 的限制。这种检验师为了保护程序,防止‘阻断服务攻击(denial-of-service attack)’,攻击者会填入一个无法加载完的请求头,让程序进入一个永远在加载中的状态。"</p>    <h3>4 选择一个合适的框架实现 Node.js REST API</h3>    <p>选择最适合项目应用场景的,才是最重要的。</p>    <h3>Express, Koa 还是 Hapi</h3>    <p>Express , Koa 和 Hapi 都可以支持浏览器调用,并且他们都支持模板和后端渲染,这里我们之罗列一小部分他们的特性。如果你的应用需要面向用户(界面),这些特性对他们是有意义的。</p>    <h3>Restify</h3>    <p>另一面, Restify 是一个专注于构建REST服务的库。它强制你使用严格模式构建你的API服务,以此保证整个系统的可维护性和可监控性。 Restify也带有自动化工具 DTrace 支持,动态监测你的程序。</p>    <p>Restify也被大量产品用在主程序里,比如 npm 和 Netflix 。</p>    <h3>5 对 Node.js REST API 进行黑盒测试</h3>    <p>检验你的REST API的最佳方式是进行黑盒测试。</p>    <p>黑盒测试是一种测试方法,就是排除任何主观因素和已知条件,检测每个功能是否都能正常使用。所以没有任何外部依赖是假数据或者桩代码(stub),整个程序是看一个整体。</p>    <p>一个可以帮助你进行Node.js REST API黑盒测试的工具 supertest 。</p>    <p>一个简单的用来检验用户返回数据的用例,如果使用 mocha 完成的话,如下:</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>也许你会问: 数据是怎样通过REST API插入进数据库的?</p>    <p>通常,一个好的写测试用例的方法,就是尽可能减少假设的状态。并且,在某些场景中,当你需要知道系统状态的时候,黑盒测试可以发现系统设计的盲点,所以你可以通过断言,实现更高的测试覆盖率。</p>    <p>所以,根据你的需要,可以用下列方式之一,用测试数据填充数据库:</p>    <ul>     <li> <p>在一个已知的数据库的子集,运行黑盒测试脚本</p> </li>     <li> <p>新建一个填充了数据的数据库,运行黑盒测试脚本</p> </li>    </ul>    <p>当然,黑盒测试不意味这你不需要单元测试,你也需要给你的API写单元测试脚本 unit tests 。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/93049a159494c0f10189f1e1dd039db9.jpg"></p>    <p>使用RisingStack的程序监听和Debug专家</p>    <p>通过Trace提高REST API质量</p>    <h3>6 使用JWT-Based,无状态身份认证</h3>    <p>你的REST API必须是无状态的,身份认证也一样。JWT (JSON Web Token) 是个经典的解决方案。</p>    <p>JWT 由三部分构成:</p>    <ul>     <li> <p>Header,包含token的类型和哈希算法</p> </li>     <li> <p>Payload,包括断言</p> </li>     <li> <p>Signature(JWT 不加密 payload,只签名)</p> </li>    </ul>    <p>在你的程序中加入 JWT-based 非常简单:</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 字段提供token。</p>    <pre>  <code class="language-javascript">curl --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" my-website.com`</code></pre>    <p>你可能注意到了,JWT不依赖任何数据层。因为JWT可以通过tokens自我验证,请求也可以包含请求有效时间。</p>    <p>当然,你也可以通过HTTPS来保护你的API安全。</p>    <h3>7 使用条件请求</h3>    <p>You can think of these headers as preconditions: if they are met, the requests will be executed in a different way.</p>    <p>条件请求根据HTTP header不同,返回不同。你可以把这些HTTP header作为先决条件:当条件符合,就会获得相应的返回。</p>    <p>These headers try to check whether a version of a resource stored on the server matches a given version of the same resource. Because of this reason, these headers can be:</p>    <p>这些header希望检测,这一版本的资源是否存在服务端,并返回对应版本的资源。所以,header可能包括如下信息:</p>    <ul>     <li> <p>最后一次修改的时间戳</p> </li>     <li> <p>一个标识不同版本entity tag</p> </li>    </ul>    <p>对应API:</p>    <ul>     <li> <p>Last-Modified (标识当前资源最后修改时间)</p> </li>     <li> <p>Etag (实体标签,标识版本)</p> </li>     <li> <p>If-Modified-Since (和 Last-Modified 一起使用)</p> </li>     <li> <p>If-None-Match (和 Etag 一起使用)</p> </li>    </ul>    <p>一个例子</p>    <p>The client below did not have any previous versions of the doc resource, so neither the If-Modified-Since , nor the If-None-Match header was applied when the resource was sent. Then, the server responds with the Etag and Last-Modified headers properly set.</p>    <p>下面,客户端先前没有任何关于 doc 资源的版本,所以在发送请求时没有 If-Modified-Since , 和 If-None-Match 信息。之后服务端返回资源,并在响应头里写 Etag 和 Last-Modified 。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1c843222612fec6ddf1eb815b8e0b8d4.png"></p>    <p>如果在请求的时候,客户端设置了 If-Modified-Since 和 If-None-Match ,一旦请求这个资源, 这个验证这个资源的版本。如果版本相同,服务器只是回应 304 - Not Modified 状态,不返回其他信息。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/853a717f2d71ff337438eb115ff7c27a.png"></p>    <h3>8 请求频率限制</h3>    <p>请求频率限制用于控制API可以被消费多少次。</p>    <p>可以通过设置HTTP 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>大部分HTTP框架支持这种写法(或通过插件支持)。举个例子,如果你用Koa,可以使用 koa-ratelimit 这个包。</p>    <p>注意,请求时间间隔可以根据API不同,设置不同。比如 GitHub时间间隔是1小时,推ter是15分钟。</p>    <h3>9 创建一个合适的API文档</h3>    <p>你写的API是给别人用的,要对别人有价值。提供一个API文档是十分重要的。</p>    <p>以下开源项目可以帮助你创建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>    <h3>10 不要错过未来的API</h3>    <p>在过去的几年中,出现了两个API查询语言,非死book的GraphQL和Netflix的Falcor。但我们为什么需要它们?</p>    <p>想象以下RESTful资源请求:</p>    <pre>  <code class="language-javascript">/org/1/space/2/docs/1/collaborators?include=email&page=1&limit=10`</code></pre>    <p>这很容易失控——如果你希望按照相同的数据结构返回数据。这是GraphQL和Falcor解决的问题。</p>    <p>关于GraphQL</p>    <p>GraphQL是一门API查询语言,运行时为完成这些查询现有数据。GraphQL提供了一套完整的,易于理解的,可以描述数据的API。让客户端有了完整描述自己需要的数据的能力,随着时间的推移,这种方式可能演变成API,成为更强大的开发工具。 </p>    <p>关于Falcor</p>    <p>Falcor一个创新的数据抓取哭,支撑着Netflix UI。Falcor允许你通过一个Node服务上的虚拟JSON操作任何后台数据。在客户端,使用远程JSON对象,通过get,set, call查找数据,数据就是API 。</p>    <h2>令人惊讶的REST API灵感</h2>    <p>如果你正想要开发Node.js REST API或者更新一个版本,我们收集了4个有价值的,现实中的例子:</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">DigitalOcean API</a></p> </li>    </ul>    <p> </p>    <p> </p>    <p>来自:http://www.w3ctech.com/topic/1968</p>    <p> </p>