跟着 Github 学习 Restful HTTP API 设计

DianeManifo 7年前
   <p>近几年提供 HTTP API 服务的公司越来越多,许多公司都把 API 作为产品重要的一部分,作为服务提供出去。而微服务的兴起,也让企业内部开始重视和频繁使用 HTTP API 。好的 HTTP API 设计容易理解、符合 RFC 标准、提供使用者便利的功能,其中经常被拿来作为教科书典范的当属 Github API 。这篇文章就通过 Github API 总结了一些非常好的设计原则,可以作为以后要编写 HTTP API 的参考。</p>    <p>注意:这篇文章只讨论设计原则,不是强制要求(API 设计者可以根据实际情况实现部分内容,甚至实现出和某些原则相反的内容),也不会给出实现的思路和细节。</p>    <h2>1. 使用 HTTPS</h2>    <p>这个和 Restful API 本身没有很大的关系,但是对于增加网站的安全是非常重要的。特别如果你提供的是公开 API,用户的信息泄露或者被攻击会严重影响网站的信誉。</p>    <p>NOTE:不要让非SSL的url访问重定向到SSL的url。</p>    <h2>2. API 地址和版本</h2>    <p>在 url 中指定 API 的版本是个很好地做法。如果 API 变化比较大,可以把 API 设计为子域名,比如 https://api.github.com/v3 ;也可以简单地把版本放在路径中,比如 https://example.com/api/v1 。</p>    <h2>3. schema</h2>    <p>对于响应返回的格式,JSON 因为它的可读性、紧凑性以及多种语言支持等优点,成为了 HTTP API 最常用的返回格式。因此,最好采用 JSON 作为返回内容的格式。如果用户需要其他格式,比如 xml ,应该在请求头部 Accept 中指定。对于不支持的格式,服务端需要赶回正确的 status code,并给出详细的说明。</p>    <h2>4. 以资源为中心的 URL 设计</h2>    <p>资源是 Restful API 的核心元素,所有的操作都是针对特定资源进行的。而资源就是 URL (Uniform Resoure Locator)表示的,所以简洁、清晰、结构化的 URL 设计是至关重要的。Github 可以说是这方面的典范,下面我们就拿 repository 来说明。</p>    <pre>  /users/:username/repos  /users/:org/repos  /repos/:owner/:repo  /repos/:owner/:repo/tags  /repos/:owner/:repo/branches/:branch</pre>    <p>我们可以看到几个特性:</p>    <ul>     <li>资源分为单个文档和集合,尽量使用复数来表示资源,单个资源通过添加 id 或者 name 等来表示</li>     <li>一个资源可以有多个不同的 URL</li>     <li>资源可以嵌套,通过类似目录路径的方式来表示,以体现它们之间的关系</li>    </ul>    <p>NOTE: 根据RFC3986定义,URL是大小写敏感的。所以为了避免歧义,尽量使用小写字母。</p>    <h2>5. 使用正确的 Method</h2>    <p>有了资源的 URL 设计,所有针对资源的操作都是使用 HTTP 方法指定的。比较常用的方法有:</p>    <table>     <thead>      <tr>       <th>Verb</th>       <th>描述</th>      </tr>     </thead>     <tbody>      <tr>       <td>HEAD</td>       <td>只获取某个资源的头部信息。比如只想了解某个文件的大小,某个资源的修改日期等</td>      </tr>      <tr>       <td>GET</td>       <td>获取资源</td>      </tr>      <tr>       <td>POST</td>       <td>创建资源</td>      </tr>      <tr>       <td>PATCH</td>       <td>更新资源的部分属性。因为 PATCH 比较新,而且规范比较复杂,所以真正实现的比较少,一般都是用 POST 替代</td>      </tr>      <tr>       <td>PUT</td>       <td>替换资源,客户端需要提供新建资源的所有属性。如果新内容为空,要设置 Content-Length 为 0,以区别错误信息</td>      </tr>      <tr>       <td>DELETE</td>       <td>删除资源</td>      </tr>     </tbody>    </table>    <p>比如:</p>    <pre>  GET /repos/:owner/:repo/issues  GET /repos/:owner/:repo/issues/:number  POST /repos/:owner/:repo/issues  PATCH /repos/:owner/:repo/issues/:number  DELETE /repos/:owner/:repo</pre>    <p>NOTE:更新和创建操作应该返回最新的资源,来通知用户资源的情况;删除资源一般不会返回内容。</p>    <h3>不符合 CRUD 的情况</h3>    <p>在实际资源操作中,总会有一些不符合 CRUD (Create-Read-Update-Delete) 的情况,一般有几种处理方法。</p>    <p>使用 POST</p>    <p>为需要的动作增加一个 endpoint,使用 POST 来执行动作,比如 POST /resend 重新发送邮件。</p>    <p>增加控制参数</p>    <p>添加动作相关的参数,通过修改参数来控制动作。比如一个博客网站,会有把写好的文章“发布”的功能,可以用上面的 POST /articles/{:id}/publish 方法,也可以在文章中增加 published:boolean 字段,发布的时候就是更新该字段 PUT /articles/{:id}?published=true</p>    <p>把动作转换成资源</p>    <p>把动作转换成可以执行 CRUD 操作的资源, github 就是用了这种方法。</p>    <p>比如“喜欢”一个 gist,就增加一个 /gists/:id/star 子资源,然后对其进行操作:“喜欢”使用 PUT /gists/:id/star ,“取消喜欢”使用 DELETE /gists/:id/star 。</p>    <p>另外一个例子是 Fork ,这也是一个动作,但是在 gist 下面增加 forks 资源,就能把动作变成 CRUD 兼容的: POST /gists/:id/forks 可以执行用户 fork 的动作。</p>    <h2>6. Query 让查询更自由</h2>    <p>比如查询某个 repo 下面 issues 的时候,可以通过以下参数来控制返回哪些结果:</p>    <ul>     <li>state:issue 的状态,可以是 open , closed , all</li>     <li>since:在指定时间点之后更新过的才会返回</li>     <li>assignee:被 assign 给某个 user 的 issues</li>     <li>sort:选择排序的值,可以是 created 、 updated 、 comments</li>     <li>direction:排序的方向,升序(asc)还是降序(desc)</li>     <li>……</li>    </ul>    <h2>7. 分页 Pagination</h2>    <p>当返回某个资源的列表时,如果要返回的数目特别多,比如 github 的 /users ,就需要使用分页分批次按照需要来返回特定数量的结果。</p>    <p>分页的实现会用到上面提到的 url query,通过两个参数来控制要返回的资源结果:</p>    <ul>     <li>per_page:每页返回多少资源,如果没提供会使用预设的默认值;这个数量也是有一个最大值,不然用户把它设置成一个非常大的值(比如 99999999 )也失去了设计的初衷</li>     <li>page:要获取哪一页的资源,默认是第一页</li>    </ul>    <p>返回的资源列表为 [(page-1)*per_page, page*per_page) 。github API 文档中还提到一个很好的点,相关的分页信息还可以存放到 Link 头部,这样客户端可以直接得到诸如 下一页 、 最后一页 、 上一页 等内容的 url 地址,而不是自己手动去计算和拼接。</p>    <h2>8. 选择合适的状态码</h2>    <p>HTTP 应答中,需要带一个很重要的字段: status code 。它说明了请求的大致情况,是否正常完成、需要进一步处理、出现了什么错误,对于客户端非常重要。状态码都是三位的整数,大概分成了几个区间:</p>    <ul>     <li>2XX :请求正常处理并返回</li>     <li>3XX :重定向,请求的资源位置发生变化</li>     <li>4XX :客户端发送的请求有错误</li>     <li>5XX :服务器端错误</li>    </ul>    <p>在 HTTP API 设计中,经常用到的状态码以及它们的意义如下表:</p>    <table>     <thead>      <tr>       <th>状态码</th>       <th>Label</th>       <th>解释</th>      </tr>     </thead>     <tbody>      <tr>       <td>200</td>       <td>OK</td>       <td>请求成功接收并处理,一般响应中都会有 body</td>      </tr>      <tr>       <td>201</td>       <td>Created</td>       <td>请求已完成,并导致了一个或者多个资源被创建,最常用在 POST 创建资源的时候</td>      </tr>      <tr>       <td>202</td>       <td>Accepted</td>       <td>请求已经接收并开始处理,但是处理还没有完成。一般用在异步处理的情况,响应 body 中应该告诉客户端去哪里查看任务的状态</td>      </tr>      <tr>       <td>204</td>       <td>No Content</td>       <td>请求已经处理完成,但是没有信息要返回,经常用在 PUT 更新资源的时候(客户端提供资源的所有属性,因此不需要服务端返回)。如果有重要的 metadata,可以放到头部返回</td>      </tr>      <tr>       <td>301</td>       <td>Moved Permanently</td>       <td>请求的资源已经永久性地移动到另外一个地方,后续所有的请求都应该直接访问新地址。服务端会把新地址写在 Location 头部字段,方便客户端使用。允许客户端把 POST 请求修改为 GET。</td>      </tr>      <tr>       <td>304</td>       <td>Not Modified</td>       <td>请求的资源和之前的版本一样,没有发生改变。用来缓存资源,和条件性请求(conditional request)一起出现</td>      </tr>      <tr>       <td>307</td>       <td>Temporary Redirect</td>       <td>目标资源暂时性地移动到新的地址,客户端需要去新地址进行操作,但是 <strong>不能</strong> 修改请求的方法。</td>      </tr>      <tr>       <td>308</td>       <td>Permanent Redirect</td>       <td>和 301 类似,除了客户端 <strong>不能</strong> 修改原请求的方法</td>      </tr>      <tr>       <td>400</td>       <td>Bad Request</td>       <td>客户端发送的请求有错误(请求语法错误,body 数据格式有误,body 缺少必须的字段等),导致服务端无法处理</td>      </tr>      <tr>       <td>401</td>       <td>Unauthorized</td>       <td>请求的资源需要认证,客户端没有提供认证信息或者认证信息不正确</td>      </tr>      <tr>       <td>403</td>       <td>Forbidden</td>       <td>服务器端接收到并理解客户端的请求,但是客户端的权限不足。比如,普通用户想操作只有管理员才有权限的资源。</td>      </tr>      <tr>       <td>404</td>       <td>Not Found</td>       <td>客户端要访问的资源不存在,链接失效或者客户端伪造 URL 的时候回遇到这个情况</td>      </tr>      <tr>       <td>405</td>       <td>Method Not Allowed</td>       <td>服务端接收到了请求,而且要访问的资源也存在,但是不支持对应的方法。服务端 <strong>必须</strong> 返回 Allow 头部,告诉客户端哪些方法是允许的</td>      </tr>      <tr>       <td>415</td>       <td>Unsupported Media Type</td>       <td>服务端不支持客户端请求的资源格式,一般是因为客户端在 Content-Type 或者 Content-Encoding 中申明了希望的返回格式,但是服务端没有实现。比如,客户端希望收到 xml 返回,但是服务端支持 Json</td>      </tr>      <tr>       <td>429</td>       <td>Too Many Requests</td>       <td>客户端在规定的时间里发送了太多请求,在进行限流的时候会用到</td>      </tr>      <tr>       <td>500</td>       <td>Internal Server Error</td>       <td>服务器内部错误,导致无法完成请求的内容</td>      </tr>      <tr>       <td>503</td>       <td>Service Unavailable</td>       <td>服务器因为负载过高或者维护,暂时无法提供服务。服务器端应该返回 Retry-After 头部,告诉客户端过一段时间再来重试</td>      </tr>     </tbody>    </table>    <p>上面这些状态码覆盖了 API 设计中大部分的情况,如果对某个状态码不清楚或者希望查看更完整的列表。</p>    <h2>9. 错误处理:给出详细的信息</h2>    <p>如果出错的话,在 response body 中通过 message 给出明确的信息。</p>    <p>比如客户端发送的请求有错误,一般会返回 4XX Bad Request 结果。这个结果很模糊,给出错误 message 的话,能更好地让客户端知道具体哪里有问题,进行快速修改。</p>    <ul>     <li>如果请求的 JSON 数据无法解析,会返回 Problems parsing JSON</li>     <li>如果缺少必要的 filed,会返回 422 Unprocessable Entity ,除了 message 之外,还通过 errors 给出了哪些 field 缺少了,能够方便调用方快速排错</li>    </ul>    <p>基本的思路就是尽可能提供更准确的错误信息:比如数据不是正确的 json,缺少必要的字段,字段的值不符合规定…… 而不是直接说“请求错误”之类的信息。</p>    <h2>10. 验证和授权</h2>    <p>一般来说,让任何人随意访问公开的 API 是不好的做法。验证和授权是两件事情:</p>    <ul>     <li>验证(Authentication)是为了确定用户是其申明的身份,比如提供账户的密码。不然的话,任何人伪造成其他身份(比如其他用户或者管理员)是非常危险的</li>     <li>授权(Authorization)是为了保证用户有对请求资源特定操作的权限。比如用户的私人信息只能自己能访问,其他人无法看到;有些特殊的操作只能管理员可以操作,其他用户有只读的权限等等</li>    </ul>    <p>如果没有通过验证(提供的用户名和密码不匹配,token 不正确等),需要返回 <strong>401 Unauthorized</strong> 状态码,并在 body 中说明具体的错误信息;而没有被授权访问的资源操作,需要返回 <strong>403 Forbidden</strong> 状态码,还有详细的错误信息。</p>    <p>NOTE:Github API 对某些用户未被授权访问的资源操作返回 <strong>404 Not Found</strong> ,目的是为了防止私有资源的泄露(比如黑客可以自动化试探用户的私有资源,返回 403 的话,就等于告诉黑客用户有这些私有的资源)。</p>    <h2>11. 限流 rate limit</h2>    <p>如果对访问的次数不加控制,很可能会造成 API 被滥用,甚至被 DDos 攻击 。根据使用者不同的身份对其进行限流,可以防止这些情况,减少服务器的压力。</p>    <p>对用户的请求限流之后,要有方法告诉用户它的请求使用情况, Github API 使用的三个相关的头部:</p>    <ul>     <li>X-RateLimit-Limit : 用户每个小时允许发送请求的最大值</li>     <li>X-RateLimit-Remaining :当前时间窗口剩下的可用请求数目</li>     <li>X-RateLimit-Rest : 时间窗口重置的时候,到这个时间点可用的请求数量就会变成 X-RateLimit-Limit 的值</li>    </ul>    <p>如果允许没有登录的用户使用 API(可以让用户试用),可以把 X-RateLimit-Limit 的值设置得很小,比如 Github 使用的 60 。没有登录的用户是按照请求的 IP 来确定的,而登录的用户按照认证后的信息来确定身份。</p>    <p>对于超过流量的请求,可以返回 <strong>429 Too many requests</strong> 状态码,并附带错误信息。而 Github API 返回的是 <strong>403 Forbidden</strong> ,虽然没有 429 更准确,也是可以理解的。</p>    <p>Github 更进一步,提供了不影响当然 RateLimit 的请求查看当前 RateLimit 的接口 <strong>GET /rate_limit</strong> 。</p>    <h2>12. Hypermedia API</h2>    <p>Restful API 的设计最好遭到 Hypermedia:在返回结果中提供相关资源的链接。这种设计也被称为 HATEOAS 。这样做的好处是,用户可以根据返回结果就能得到后续操作需要访问的地址。</p>    <p>比如访问 <a href="/misc/goto?guid=4958833289556298570" rel="nofollow,noindex">api.github.com</a> ,就可以看到 Github API 支持的资源操作。</p>    <h2>13. 编写优秀的文档</h2>    <p>API 最终是给人使用的,不管是公司内部,还是公开的 API 都是一样。即使我们遵循了上面提到的所有规范,设计的 API 非常优雅,用户还是不知道怎么使用我们的 API。最后一步,但非常重要的一步是:为你的 API 编写优秀的文档。</p>    <p>对每个请求以及返回的参数给出说明,最好给出一个详细而完整地示例,提醒用户需要注意的地方……反正目标就是用户可以根据你的文档就能直接使用 API,而不是要发邮件给你,或者跑到你的座位上问你一堆问题。</p>    <h2>参考资料</h2>    <ul>     <li><a href="/misc/goto?guid=4958863874080865968" rel="nofollow,noindex">Github API v3</a></li>     <li><a href="/misc/goto?guid=4958833289831923922" rel="nofollow,noindex">RESTful API 设计指南</a></li>     <li><a href="/misc/goto?guid=4959729263776350389" rel="nofollow,noindex">REST接口设计规范</a></li>     <li><a href="/misc/goto?guid=4959662370598498842" rel="nofollow,noindex">Restful API 首次被提出的论文:Architectural Styles and the Design of Network-based Software Architectures</a></li>    </ul>    <p> </p>    <p>来自:http://cizixs.com/2016/12/12/restful-api-design-guide</p>    <p> </p>