根据REST APIs的成熟度模型 ,此规范关注的是Level 2
的APIs。
1 设计指南
HTTP APIs主要由四部分组成 : #HTTP, #URL, 资源
,资源的表述
。
根据这些组成部分,按照以下3个步骤设计APIs。
1.1 基于资源
设计API
设计HTTP APIs的首要任务是识别出业务领域中的资源。资源是对服务端提供的服务进行分解、组合后的一个被命名的抽象概念。
有很重要一点需要明确 : 资源
≠
数据表,它们两个之间并没有直接的映射关系。如果直接把数据存储结构映射为资源,则只会让资源无法有效的表达业务需要,也会造成资源本身和底层存储的紧耦合。
资源的设计是以名词
为中心的。比如今天的天气
是一个资源; 而获取今天的天气则不是,它代表的是对今天的天气
资源的一个读取操作。基于此我们可以抽象出来一个天气
的资源。
1.2 基于URL
标识资源
识别出资源
后,则需要为其分配一个#URL进行标识。
总结来说就是
资源 : URL
的关系就是1 : N
的关系。
比如上面提到的天气
和今天的天气
这两个资源,可以用如下的#URL进行标识。
资源 | URL |
---|---|
天气 | /weather |
今天的天气 | /weather/today |
今天的天气 | /weather/2018-04-01 ,今天是2018-04-01 |
资源名
(资源的名字)体现在#URL中的Path
部分。
关于
资源名
采用单数还是复数的问题,这里统一为单数(即使代表的是一个集合资源)。原因有 3 :
- 一致性 : 中文中并无复数的概念,可保持一致。
- 无二义性 : 比如news,既是单数也是复数。所以就不必追求它们的单数或者复数形式形式;基于同样的原则,那么原本就是单数的名词,也无需刻意追求复数形式。
- 简单性 : 英文名词的复数形式并不统一(比如order > orders, history > histories),使用单数可以避免团队成员对于这些差异的不同理解与争执。
资源
存在子资源的情况下,可以把子资源提升为顶层的资源。比如有一个订单资源/order/{order_id}
,订单中包含2件物品。
# 不推荐 单个子资源
/order/{order_id}/item/{item_id}
# 推荐 单个子资源
/order-item/{order_item_id}
# 推荐 子资源集合
/order/{order_id}/item
1.3 基于HTTP
和JSON
操作URL
标识的资源
在标识出资源
以后,就可以使用#HTTP通过#JSON来操作资源了。
- 使用#HTTP Method来映射对资源的操作请求(CRUD或者其他)。
- 使用#HTTP Header携带请求/响应所需的元数据信息。
- 使用#HTTP Stauts Code表示HTTP协议层面的响应状态。
- 使用#JSON作为数据交换格式。
2 URL
URL遵循RFC3986规范,由以下几部分组成。
https://api.linianhui.test:8080/user/disabled?first_name=li#title
\___/ \______________________/\_____________/\___________/\____/
| | | | |
scheme authority path query fragment
URL的命名规则#URL命名规则。
参考资料 :
3 HTTP
3.1 HTTP Method
面向资源设计的HTTP APIs中,绝大部分的操作都是CRUD(Create,Read,Update,Delete)
,都可以映射为某一个HTTP Method。其余的无法映射的操作一般存在两种解决方案 :
- 抽象出新的资源,比如禁用用户的操作。假设用户的资源是
/user
,那么可以抽象出来一个被锁定的用户的资源/user/disabled
。如此以来,- 禁用用户 :
POST /user/disabled
或者PUT /user/disabled/{user_id}
。 - 取消禁用 :
DELETE /user/disabled/{user_id}
。 - 获取被禁用的用户列表 :
GET /user/disabled
。
- 禁用用户 :
- 如果上面的方式无法满足需要,则可以采用
POST
和URL/动词
的组合。还拿上面的举例 :- 禁用用户 :
POST /user/{user_id}/disable
或者PUT /user/{user_id}/disable
。 - 取消禁用 :
DELETE /user/{user_id}/disable
。 - 获取被禁用的用户列表 :
GET /user?status=DISABLED
。
- 禁用用户 :
3.1.1 Names
HTTP Method Name | Safe | Idempotent | 描述说明 |
---|---|---|---|
GET | ✔ | ✔ | 获取一个资源 |
PUT | ✘ | ✔ | 更新或创建一个资源(完整替换) |
PATCH | ✘ | ✘ | 更新一个资源(部分更新) |
DELETE | ✘ | ✔ | 删除一个资源 |
POST | ✘ | ✘ | 创建,或者不满足以上四个Method语义的所有操作 |
PATCH和POST都是不安全
且不幂等
的,差异在于PATCH仅是用于部分更新资源, 而且是一个可选支持的HTTP Method,可能会存在一些代理、网关等组件不支持的情况,所以推荐用POST来代替它。
3.1.2 Semantics
每一个HTTP Method都具有一下3个HTTP协议层面的语义。
HTTP Method Semantics | 含义 |
---|---|
Safe | 操作不会对资源产生副作用,不会修改资源。 |
Idempotent | 执行一次和重复执行N次,结果是一样的。 |
Cacheable | 可以被缓存。 |
参考资料 :
- https://tools.ietf.org/html/rfc7231#section-4
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
3.2 HTTP Header
Http Header的用途在于携带HTTP Request
和HTTP Response
的元数据信息。
格式为Name:Value
: Name不区分大小写,通常都采用首字母大写,-
分隔的写法,比如Content-Type
。按其用途可以分为如下4类 :
- 通用类 : 描述请求和响应的。
- 请求类 : 描述请求的。
- 响应类 : 描述响应的。
- 表述类 : 描述请求和响应的
Body
部分的。
HTTP APIs中常用到的Headers :
HTTP Header Name | 描述说明 | 示例 |
---|---|---|
Accept | 客户端期望服务器返回的数据格式。 | Accept:application/json |
Accept-Charset | 客户端期望服务器返回的数据的字符集。 | Accept-Charset:utf-8 |
Content-Type | 描述Body 的数据类型。 | Content-Type:application/json |
Content-Encoding | 描述Body 的编码方式。 | Content-Encoding: gzip |
Content-Length | 描述Body 的长度。 | Content-Length: 1234 |
Location | Response 中提供给客户端的连接 | Location: /user/1 |
HTTP Request:
POST /xxx HTTP/1.1
Accept: application/json;
Accept-Charset: utf-8
Content-Type: application/json;charset=utf-8
{
"x":1,
"y":2
}
HTTP Response:
HTTP/1.1 201 Created
Content-Type: application/json;charset=utf-8
Content-Encoding: gzip
Location: /xxx/1
Request-Id: {id}
Request-Id
可以由Http Request
传入,也可以由服务端生成,追加此信息到log
中,便于服务端追踪请求。
参考资料 :
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
- https://tools.ietf.org/html/rfc7231#section-5
- https://tools.ietf.org/html/rfc7231#section-7
3.3 HTTP Stauts Code
HTTP Status Code用来指示HTTP协议层面的请求状态
。它由一个数字和一个描述消息构成,比如200 OK
。有以下几类状态码 :
2xx
: 执行成功。4xx
: 客户端的错误,通常情况下客户端需要修改请求然后再次发送请求。5xx
: 服务端的错误。
HTTP Status Code | 描述说明 |
---|---|
200 OK | 执行成功。 |
201 Created | 资源创建成功,应该在HTTP Response Header 中返回Location 来提供新创建资源的URL地址。 |
202 Accepted | 服务端已经接受了请求,但是并未处理完成,适用于一些异步操作。 |
204 No Content | 执行成功,但是不会在HTTP Response Body 中放置数据。 |
400 Bad Request | 客户端请求错误,客户端应该根据HTTP Response Body 中的错误描述来修改请求,然后才能再次发送。 |
401 Unauthorized | 客户端未提供授权信息。 |
403 Forbidden | 客户端无权访问(客户端可能已经提供了授权信息,但是权限不够)。如果出于信息隐藏的目的,也可以使用404 Not Found 来替代。 |
404 Not Found | 客户端请求的资源不存在。 |
405 Method Not Allowed | 客户端使用了不被允许的HTTP Method。比如某一个URL只允许POST ,但是客户端采用了GET 。 |
406 Not Acceptable | 客户端发送的Accept 不被支持。比如客户端发送了Accept:application/xml ,但是服务器只支持Accept:application/json 。 |
409 Conflict | 客户端提交的数据过于陈旧,和服务端的存在冲突,需要客户端重新获取最新的资源再发起请求。 |
415 Unsupported Media Type | 客户端发送的Content-Type 不被支持。比如客户端发送了Content-Type:application/xml ,但是服务器只支持Content-Type:application/json 。 |
429 Too Many Requests | 客户端在指定的时间内发送了太多次数的请求。用于限速,比如只允许客户端在1分钟内发送100次请求,客户端在发送101次请求的时候,会得到这样的响应。 |
500 Internal Server Error | 服务器遇见了未知的内部错误。 |
501 Not Implemented | 服务器还未实现次功能。 |
503 Service Unavailable | 服务器繁忙,暂时无法处理客户端的请求。 |
参考资料 :
- https://tools.ietf.org/html/rfc7231#section-6
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
- https://httpstatuses.com/
3.4 HTTP Error Response
虽然#HTTP Stauts Code有4xx
和5xx
的状态码来表示哪里出错了,但是其代表的只是HTTP协议层面
的错误描述,它无法提供和业务相关的更具体错误描述。基于此种情况,我们需要设计一套描述业务层面错误的数据结构 :
{
"method": "POST",
"url": "/book",
"statusCode": 400,
"errors": [
{
"code": "name",
"value": null,
"message": "书名不能为空。"
},
{
"code": "price",
"value": -1,
"message": "价格不能为负数"
}
],
"extra": null
}
以上这个数据结构仅在状态码为4xx
和5xx
出现的时候才会使用;2xx
的时候则不包含此数据结构。
code
字段可以是一些出错的字段名、某一错误类别(比如NO_PERMISSION
这种全局唯一的常量字符串)等等。
{
"method": "DELETE",
"url": "/book/1",
"statusCode": 403,
"errors": [
{
"code": "NO_PERMISSION",
"value": "BOOK:DELETE",
"message": "没有BOOK:DELETE的权限"
}
],
"extra": null
}
参考资料 :
- Problem Details for HTTP APIs : https://tools.ietf.org/html/rfc7807
4 JSON
JSON是一种应用非常广泛的数据交换格式。其包含6种基本的数据类型。
JSON数据类型 | 示例 |
---|---|
string | "lnh" |
number | 1.23 |
array | [...] |
object | {...} |
bool | true/false |
null | null |
- JSON的命名规则#JSON命名规则。
- JSON中没有原生的日期和时间类型,应该遵循#Date Time的要求,使用
string
类型表示。 - JSON中出现的和国际化相关的数据遵循#i18n中的要求。
null
值的字段不能忽略掉,应该显式的表示为"field_name":null
。
JSON示例 :
{
"first_name": "li",
"lase_name": "nianhui",
"gender": "MALE",
"age": 123,
"tags": ["coder"],
"is_admin": true,
"address": null,
"country_code":"CN",
"currency_code":"CNY",
"language_code":"zh",
"locale_code":"zh-CN",
"created_at":"2018-01-02T03:04:05.128Z+08:00",
"time_zone":"+08:00"
}
参考资料 :
5 版本化
在Level 2
的HTTP APIs中,虽然我们推荐也努力使得我们的APIs不做不兼容的改动,但是依然无法彻底的避免不兼容的升级。这就使得我们不得不对APIs进行版本化管理。通常有以下 3 种方案 :
- URL
GET http://api.linianhui.com/v1/weather HTTP/1.1
- Request Header
GET http://api.linianhui.com/weather HTTP/1.1 Api-Version: v1
- Request Header (Accept Header)
GET http://api.linianhui.com/weather HTTP/1.1 Accept: application/vnd.v1+json
在Level 2
的API中优先推荐使用方案1(URL
)。理由是其更直观,便于实现,便于日志追踪。
6 命名规则
规则名称 | 说明 | 取值范围 |
---|---|---|
all-lower-hyphen-case | 采用- 分隔符的全小写 | a-z 0-9 - |
all_lower_underscore_case | 采用_ 分隔符的全小写 | a-z 0-9 _ |
ALL_UPPER_UNDERSCORE_CASE | 采用_ 分隔符的全大写 | A-Z 0-9 _ |
6.1 URL
URL组件 | 命名规则 |
---|---|
scheme | all-lower-hyphen-case |
authority | all-lower-hyphen-case |
path | all-lower-hyphen-case |
query | all_lower_underscore_case |
fragment | all-lower-hyphen-case |
URL的query部分是name=value
而不是key=value
,URL支持name重复存在,Web服务端框架绝大部分都支持直接映射为数组。
此外命名规则约束的是name
部分,而不关心value
部分,value
部分应该采用urlencode
进行编码。示例 :
https://api.my-server.com/v1/user-stories?dipplay_names=abc&display_names=efg
服务端会得到一个类型为数组的dispaly_names
参数。
display_names = [
"abc",
"efg"
];
6.2 JSON
JSON | 命名规则 |
---|---|
filed_name | all_lower_underscore_case |
filed_value | 无要求 |
ENUM_FILED_VALUE | ALL_UPPER_UNDERSCORE_CASE |
ENUM_FILED_VALUE
用于表示枚举字段,用全大写和_
分隔符,以示和普通的字符串进行区分。示例 :
{
"first_name":"li",
"lase_name":"nianhui",
"gender":"MALE",
"remark":"描述信息",
"age":1234
}
参考资料 :
- https://support.google.com/webmasters/answer/76329
- https://stackoverflow.com/questions/5543490/json-naming-convention
- https://tools.ietf.org/html/rfc3986#section-2.4
7 Date Time
日期和时间采用RFC3339中定义的通用的格式。表示方法如下 :
格式 | 组成部分 | 示例 |
---|---|---|
date-fullyear | 4位数的年份 | 2018 |
date-month | 2位数的月份 | 04 |
date-mday | 2位数的日期 | 01 |
time-hour | 2位数的小时 | 02 |
time-minute | 2位数的分钟 | 08 |
time-second | 2位数的秒数 | 59 |
time-secfrac | 秒的分数部分 | .256 |
time-numoffset | + /- time-hour :time-minute | +08:00 |
time-offset | Z /time-numoffset | Z+08:00 |
partial-time | time-hour :time-minute :time-second time-secfrac | 02:08:59.256 |
full-date | date-fullyear -date-month -date-mday | 2018-04-01 |
full-time | partial-time time-offset | 02:08:59.256+08:00 |
date-time | full-date Tfull-time | 2018-04-01T02:08:59.256Z+08:00 |
扩展 : date-fullyear-month
(年月)可表示为date-fullyear
-date-month
,比如2018-04
。
- 日期和时间应该满足上面表格中定义的格式。
- 优先采用UTC时间(即Z+00:00)。即使没有跨时区的要求,也必须携带时区偏移信息,比如
2018-04-01T02:08:59.256Z+08:00
。
参考资料 :
- Date and Time Formats - ISO 8601 : https://www.w3.org/TR/NOTE-datetime
- Date and Time on the Internet: Timestamps ( RFC 3339 ) : https://tools.ietf.org/html/rfc3339#section-5.6
8 i18n
国家代码 : 采用Country CodeISO 3166 alpha-2
版本(2个字母)。示例 :
- 中国 :
CN
- 美国 :
US
- 其他 : https://en.wikipedia.org/wiki/ISO_3166-2#Current_codes
货币代码 : 采用Currency CodeISO 4217:2015
版本(3个字母)。示例 :
- 人民币 :
CNY
- 美元 :
USD
- 其他 : https://en.wikipedia.org/wiki/ISO_4217#Active_codes
语言代码 : 采用Language CodeISO 639-1:2002
版本(2个字母)。示例 :
- 中文 :
zh
- 英文 :
en
- 其他 : https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
区域代码 : 由Language Code和Country Code组合而成。示例 :
zh-CN
en-US
参考资料 :
- https://www.iso.org/iso-3166-country-codes.html
- https://www.iso.org/iso-4217-currency-codes.html
- https://www.iso.org/iso-639-language-codes.html
9 其他
待完善...
9.1 HTTP Request 公共查询参数
参数用途 | 参数名 | 取值范围 |
---|---|---|
分页 | page page_size | >=1 |
排序 | sort | {field_name} {asc\desc},{field_name} {asc\desc} |
区间 | {field_name}_begin {field_name}_end | 无要求 |
时间 | {field_name}_at | 无要求 |
示例:
GET /user
?page=2
&page_size=10
&sort=name,age desc
&created_at_begin=2018-01-01
&created_at_end=2018-06-01
上面的查询代表的含义 : 按照name
升序和age
倒序的排序方式;获取created_at
时间位于2018-01-01
和2018-06-01
区间内;按照每页10
条数据,获取第2
页的数据。
9.2 HTTP Response 分页数据结构
在分页请求的时候,API会返回分页后的数据和分页的信息。
{
"page": 2,
"page_size": 10,
"total_count": 100,
"items":[
{...},
{...},
]
}
10 示例
... 待补充
11 参考资料
本人的解读REST系列博客 : 理解REST
REST APIs 成熟度模型 : https://martinfowler.com/articles/richardsonMaturityModel.html
PayPal的API设计指南 : https://github.com/paypal/api-standards
REST架构风格的出处 : 架构风格与基于网络的软件架构设计(by Roy Thomas Fielding)论文。
- 英文版 : https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
- 中文版 : https://www.infoq.cn/article/web-based-apps-archit-design/
HTTP APIs 四大组成部分(HTTP, URI, MIME, JSON)
- HTTP/1.1 ( RFC7230-7235 ) : https://tools.ietf.org/html/rfc7230
- URI ( RFC 3986 ) : https://tools.ietf.org/html/rfc3986
- MIME ( RFC 2387 ) : https://tools.ietf.org/html/rfc2387
- JSON : http://json.org/
Hypermedia
- JSON Schema: http://json-schema.org/
URL模板
- URI Template ( RFC6570 ) : https://tools.ietf.org/html/rfc6570
彩蛋:NO SOAP!