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. Web Linking
    2. 9.2. 实例
    3. 9.3. 更多参考:
  10. 10. Fields (Embed or Expansion)
    1. 10.1. OData
    2. 10.2. 示例
    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
  • Query (Parameters): 比较乱,但也建议用 snake_case,参见 github weibo
  • Request Body (JSON): 建议用 JSON 来上传数据,JSON 中使用 sanke_case
  • Response Data (JSON):这个业界还是挺统一的,全部使用小写,多个单词用 sanke_case


Data Convention

Format

request 和 response body 永远是 JSON

Data Type

数据类型永远匹配。

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

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

Timestamp

延伸阅读:

参考 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

Web Linking

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

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

实例

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

1
2
3
4
5
6
$ 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"

更多参考:


Fields (Embed or Expansion)

Auto loading related resource representations

OData

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

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

示例

GET /tickets?fields=id,subject,customer_name,updated_at&state=open&sort=-updated_at

有时候,一个请求能多取出来的数据,不要放2个请求。很多时候,我们需要在一个请求里,把相关的东西请求出来。但这个,Grape 里没有找到相应的实现。

GET /ticket/12embed=customer.name,assigned_user

更多参考


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.
  2. 关于这些定义,其实最好的参考是 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
{
"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