RESTful API 最佳实践

  1. 1. 🔅 Brief
  2. 2. 🔅 Versioning
  3. 3. 🔅 Path or Query
    1. 3.1. > Path variables
    2. 3.2. > Query perms/argument
    3. 3.3. > 关于 Path 和 Parameter 的最佳实践
  4. 4. 🔅 Name Convention
  5. 5. 🔅 Data Convention
    1. 5.1. > Format
    2. 5.2. > Data Type
    3. 5.3. > Timestamp
  6. 6. 🔅 Authentication
    1. 6.1. > JSON Web Token
    2. 6.2. > OAuth 2.0
  7. 7. 🔅 Envelope (信封)
    1. 7.1. > 通过参数来开启
  8. 8. 🔅 跨域
    1. 8.1. > “跨域资源共享”(Cross Origin Resource Sharing, CORS)
    2. 8.2. > JSONP | callback (optional)
  9. 9. 🔅 Pagination
    1. 9.1. > 标准
    2. 9.2. > Web Linking
    3. 9.3. > 实例
    4. 9.4. > 更多参考:
  10. 10. 🔅 (Embed or Expansion) & Fields
    1. 10.1. > Expand
    2. 10.2. > Fields
    3. 10.3. > 更多参考
  11. 11. 🔅 SEARCH (filtering | sorting | searching)
    1. 11.1. > $filter
    2. 11.2. > $orderby
    3. 11.3. > $select
    4. 11.4. > $skip
    5. 11.5. > $top
  12. 12. 🔅 HTTP status codes
  13. 13. 🔅 ERROR
    1. 13.1. > 参考
  14. 14. 🔅 Caching
    1. 14.1. > ETag
    2. 14.2. > Last-Modified
  15. 15. 🔅 Rate Limit
  16. 16. 🔅 Custom HTTP Header
  17. 17. 🔅 REF::

RESTful 更好的架构指导


🔅 Brief

接口是一种双方的约定,一旦定义好之后不能更改,只能升级。
但后台的实现可以随意改变,这样就决定了这种接口定义的重要性。

RESTful 将互联网内容定义为资源,所有的资源都有其唯一的地址(URI),对应每个资源也都有其操作的动作 (Actions: Get/Post/Patch/Update/Delete)。

本文主要参考:

Facebook 用了 GapleQL,不在 RESTful 参考范围内。


🔅 Versioning

在 API 上加入版本信息可以有效的防止用户访问已经更新了的 API,同时也能让不同主要版本之间平稳过渡,目前主流的两种方案:

  • HTTP HEAD
  • URL Path /v2

关于是否将版本信息放入 url 还是放入请求头有过争论。学术界说它应该放到 header 里面去,但是如果放到 URL 里面我们就可以跨版本的访问资源了。。(参考 openstack)。

strip 使用的方法就很好:它的 url 里面有主版本信息,同时请求头俩面有子版本信息。这样在子版本变化过程中 url 的稳定的。变化有时是不可避免的,关键是如何管理变化。完整的文档和合理的时间表都会使得 API 使用者使用的更加轻松。


🔅 Path or Query

As per the REST standards

> Path variables

Used for the direct action on the resources, like a contact or a song, will return respective data.

GET /api/resource/songid
GET /api/resource/contactid

> Query perms/argument

Used for the in-direct resources like metadata of a song, it will return the genres data for that particular song.

GET /api/resource/songid?metadata=genres

> 关于 Path 和 Parameter 的最佳实践


🔅 Name Convention

URL length limit of 2,083 characters.

可分为 4 个部分:

  • Path: 一般都是用下划线 snake_case,参见 github, twitter, weibo, 中划线的例子 Instagram
  • Query (Parameters): 也建议用 snake_case,参见 github, weibo, ins
  • Request Body / Response Data (JSON): 建议用 JSON 来上传数据,JSON 中的 key 全部使用小写,多个单词用 sanke_case

🔅 Data Convention

> Format

request 和 response body 永远是 JSON

> Data Type

数据和类型永远匹配,不会出现约定的是 string,而返回数字或 null。

type default value
number 0, 1, -1
string ‘’
array []
object {}
null null

输出的数据结构中 空对象 字段的值一律为 null

> Timestamp

timestamp 使用 ISO 8601 的标准输出。

参考 HTTP Date


🔅 Authentication

> JSON Web Token

支持通过登录接口使用账号密码获取,在请求接口时使用 Authorization: Bearer #{token} 头标或者 token 参数的值的方式进行验证。

> OAuth 2.0

Authen 现在都是通过 OAuth 2 了,为了支持 JS 项目,需要通过 Parameter 传递 access_token

1
2
3
https://api.weibo.com/2/direct_messages/conversation.json?access_token=2.00jU2OJBfj3PXCb134b9e1734Bx88D&count=30&since_id=3708839931591097&uid=1307864360

https://api.weibo.com/2/direct_messages/conversation.json?access_token=2.00jU2OJBfj3PXCb134b9e1734Bx88D&count=30&uid=1307864360

Credentials if it receives a 401 Unauthorized status code from the server.

OAuth 2 should be used to provide secure token transfer to a third party. OAuth 2 uses Bearer tokens & also depends on SSL for its underlying transport encryption.


🔅 Envelope (信封)

Don’t use an envelope by default, but make it possible when needed

一些时候,客户端拿不到 HTTP Header 信息,所以需要外面包一层,但。。。没这种情况吧。

> 通过参数来开启

1
http://xx.com/api/v1/users?envelope=true

返回数据中会在外层带上一些额外信息:

1
2
3
4
5
{
"status": 0,
"message": "",
"data": {} //只允许object, 数组或数值类需要用对象包装
}

🔅 跨域

跨域是一个比较麻烦的问题。

> “跨域资源共享”(Cross Origin Resource Sharing, CORS)

简单示例:

1
2
3
4
5
6
7
8
9
10
$ curl -i https://api.example.com -H "Origin: http://example.com"

HTTP/1.1 302 Found

$ curl -i https://api.example.com -H "Origin: http://example.com"

HTTP/1.1 302 Found
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link, X-Total-Count
Access-Control-Allow-Credentials: true

预检请求的响应示例:

1
2
3
4
5
6
7
8
9
$ curl -i https://api.example.com -H "Origin: http://example.com" -X OPTIONS

HTTP/1.1 302 Found
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With
Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE
Access-Control-Expose-Headers: ETag, Link, X-Total-Count
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true

> JSONP | callback (optional)

H5 跨域访问时,需要通过 JSONP 来进行 GET 请求,所以支持 JSONP 还是很有用的。

如果在任何 GET 请求中带有参数 callback ,且值为非空字符串,那么接口将返回如下格式的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ curl http://api.example.com/#{RESOURCE_URI}?callback=foo

# 返回 JSON

foo({
"meta": {
"status": 200,
"X-Total-Count": 542,
"Link": [
{"href": "http://api.example.com/#{RESOURCE_URI}?cursor=0&count=100", "rel": "first"},
{"href": "http://api.example.com/#{RESOURCE_URI}?cursor=90&count=100", "rel": "prev"},
{"href": "http://api.example.com/#{RESOURCE_URI}?cursor=120&count=100", "rel": "next"},
{"href": "http://api.example.com/#{RESOURCE_URI}?cursor=200&count=100", "rel": "last"}
]
},
"data": // data
})

If supplied, the response will use the JSONP format with a callback of the given name. The usefulness of this parameter is somewhat diminished by the requirement of authentication for requests to this endpoint.

Example Values: processTweets


🔅 Pagination

> 标准

在输入时,通过 URL Paramaters 在参数中传入 offset, limit, page 来控制分页的数据

  • page: The page to return (default: 1)
  • limit: The number of entries to return per page (default: 30, maximum: 100)
  • offset: offset 和 page 只能用一个,有 offset,page 不用
1
http://api.example.com/res?page=1&limit=30

返回时通过以下字段进行分页的标准输出对象:

1
2
3
4
5
6
7
8
9
{
"data": [{}, {}],
"pagination": {
"page": 1,
"limit": 30,
"total": 200,
"pages": 7
}
}

> Web Linking

新的标准 RFC5988 推荐将分页信息放到 Link Header 里面:

使用 Link Header 的 API 应该返回一系列组合好了的 url 而不是让用户自己再去拼。这点在基于游标的分页中尤为重要。例如下面,来自 Github 的文档

> 实例

如果是第二页,会返回这样的地址:它返回了 Link,包含了下一页和最后一页。

1
2
3
4
5
6
7
$ curl -I "https://api.github.com/search/code?q=addClass+user:mozilla&page=2"

HTTP/1.1 200 OK
Server: GitHub.com
Date: Thu, 03 Mar 2016 02:35:57 GMT
Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=3>; rel="next", <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last", <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=1>; rel="first", <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=1>; rel="prev"

> 更多参考:


🔅 (Embed or Expansion) & Fields

Auto loading related resource representations

用户有时需要一些自定义请求内容,把多个相关资源组合在一起返回使用,如果同时发多个请求,一来耗费网络资源,再者增加出错概率,同时也给客户端带来不便。

所以在实践中加入了 Expansion 的功能,用于实现上述需求。在该需求中,普遍的做法是加入 expand 字段,在其后,通过拼接新的对象名,来实现扩展的对象数据获取。也有不少实践中使用 embed,但意思都是相同的。

RESTful 在这一块实现的并不好,它没有一个共同的库来处理这些相同的逻辑,反之,GraphQL 则重点在这一块,不仅定义清晰,而且像 Applo 这个的库,实现了大多数的基准需求。降低了开发门槛和增加效率。

> Expand

$ expand

GET /ticket/12?expand=customer,customer.address,assigned_user

> Fields

$ fields

Directs that related records should be retrieved in the record or collection being retrieved.

客户端不是需要所有的字段,可以让客户自己选。

GET /tickets?fields=id,subject,customer_name,updated_at

> 更多参考


🔅 SEARCH (filtering | sorting | searching)

像我们之前所做的按各种条件进行筛选,都只是 filtering,当然也有排序。

Method URL
GET /tickets?q=return&state=open&sort=-priority,created_at

> $filter

Specifies an expression or function that must evaluate to ‘true’ for a record to be returned in the collection.

> $orderby

Determines what values are used to order a collection of records.

> $select

Specifies a sub set of properties to return.

> $skip

Sets the number of records to skip before it retrieves records in a collection.

> $top

Determines the maximum number of records to return.

总结

  1. 对于经常使用的搜索查询,我们可以为他们设立别名,这样会让 API 更加优雅。例如:
get /tickets?q=recently_closed -> get /tickets/recently_closed.
  1. 关于这些定义,其实最好的参考是 OData http://www.odata.org/documentation/

🔅 HTTP status codes

HTTP defines a bunch of meaningful status codes that can be returned from your API. These can be leveraged to help the API consumers route their responses accordingly. I’ve curated a short list of the ones that you definitely should be using:

The API should always return sensible HTTP status codes. API errors typically break down into 2 types: 400 series status codes for client issues & 500 series status codes for server issues. At a minimum, the API should standardize that all 400 series errors come with consumable JSON error representation. If possible (i.e. if load balancers & reverse proxies can create custom error bodies), this should extend to 500 series status codes.

关于 RESTful Status Code 的更多详解和案例,更写一文:RESTful Status in Deep


🔅 ERROR

Error 一般是在 Status 4XX & 5XX 情况下出现的。

常用的标准格式有两种,Error Object & Error Array:如下

为什么是一个数组?因为 Form 表单式的可能有多个错误,希望一次性返回。

1
2
3
4
{
"code": 1024,
"message": "Validation Failed"
}
1
2
3
4
5
"errors": [{
"code" : 5432,
"field": "first_name",
"message": "First name cannot have fancy characters"
}]

> 参考

Twitter

1
2
3
4
5
6
7
8
{
"errors": [
{
"code": 215,
"message": "Bad Authentication data."
}
]
}

Weibo:

1
2
3
4
5
{
"error": "source paramter(appkey) is missing",
"error_code": 10006,
"request": "/2/statuses/public_timeline.json"
}

🔅 Caching

HTTP provides a built-in caching framework! All you have to do is include some additional outbound response headers and do a little validation when you receive some inbound request headers.

There are 2 approaches: ETag and Last-Modified

> ETag

When generating a request, include a HTTP header ETag containing a hash or checksum of the representation. This value should change whenever the output representation changes. Now, if an inbound HTTP requests contains a If-None-Match header with a matching ETag value, the API should return a 304 Not Modified status code instead of the output representation of the resource.

> Last-Modified

This basically works like to ETag, except that it uses timestamps. The response header Last-Modified contains a timestamp in RFC 1123 format which is validated against If-Modified-Since. Note that the HTTP spec has had 3 different acceptable date formats and the server should be prepared to accept any one of them.


🔅 Rate Limit

为了避免请求泛滥,给 API 设置速度限制很重要。为此 RFC 6585 引入了 HTTP 状态码 429(too many requests)。加入速度设置之后,应该提示用户,至于如何提示标准上没有说明,不过流行的方法是使用 HTTP 的返回头。

下面是几个必须的返回头(依照 twitter 的命名规则):

  • X-Rate-Limit-Limit :当前时间段允许的并发请求数
  • X-Rate-Limit-Remaining:当前时间段保留的请求数。
  • X-Rate-Limit-Reset:当前时间段剩余秒数

Q: 为什么使用当前时间段剩余秒数而不是时间戳?

时间戳保存的信息很多,但是也包含了很多不必要的信息,用户只需要知道还剩几秒就可以再发请求了这样也避免了 clock skew 问题。


🔅 Custom HTTP Header

  • User-Agent
  • X-Server-Name
  • X-Server-IP

🔅 REF::

http://zh.wikipedia.org/wiki/REST
http://en.wikipedia.org/wiki/Representational_state_transfer