Timetombs

泛义的工具是文明的基础,而确指的工具却是愚人的器物

66h / 117a
,更新于 2024-04-27T21:37:28Z+08:00 by   25c71be

[代码规范] HTTP APIs 设计/规范指南

版权声明 - CC BY-NC-SA 4.0

根据REST APIs的成熟度模型 ,此规范关注的是Level 2的APIs。
REST APIs 成熟度模型

1 设计指南

HTTP APIs主要由四部分组成 : #HTTP, #URL, 资源资源的表述

资源的表述通常都采用#JSON格式,故而下文使用#JSON代指资源的表述

根据这些组成部分,按照以下3个步骤设计APIs。

1.1 基于资源设计API

设计HTTP APIs的首要任务是识别出业务领域中的资源。资源是对服务端提供的服务进行分解、组合后的一个被命名的抽象概念。

有很重要一点需要明确 : 资源数据表,它们两个之间并没有直接的映射关系。如果直接把数据存储结构映射为资源,则只会让资源无法有效的表达业务需要,也会造成资源本身和底层存储的紧耦合。

资源的设计是以名词为中心的。比如今天的天气是一个资源; 而获取今天的天气则不是,它代表的是对今天的天气资源的一个读取操作。基于此我们可以抽象出来一个天气的资源。

1.2 基于URL标识资源

识别出资源后,则需要为其分配一个#URL进行标识。

  1. 一个资源可以有多个#URL
  2. 一个#URL只能标识一个资源

总结来说就是资源 : URL的关系就是1 : N的关系。

比如上面提到的天气今天的天气这两个资源,可以用如下的#URL进行标识。

资源URL
天气/weather
今天的天气/weather/today
今天的天气/weather/2018-04-01,今天是2018-04-01

资源名(资源的名字)体现在#URL中的Path部分。

关于资源名采用单数还是复数的问题,这里统一为单数(即使代表的是一个集合资源)。原因有 3 :

  1. 一致性 : 中文中并无复数的概念,可保持一致。
  2. 无二义性 : 比如news,既是单数也是复数。所以就不必追求它们的单数或者复数形式形式;基于同样的原则,那么原本就是单数的名词,也无需刻意追求复数形式。
  3. 简单性 : 英文名词的复数形式并不统一(比如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 基于HTTPJSON操作URL标识的资源

在标识出资源以后,就可以使用#HTTP通过#JSON来操作资源了。

  1. 使用#HTTP Method来映射对资源的操作请求(CRUD或者其他)。
  2. 使用#HTTP Header携带请求/响应所需的元数据信息。
  3. 使用#HTTP Stauts Code表示HTTP协议层面的响应状态。
  4. 使用#JSON作为数据交换格式。

2 URL

URL遵循RFC3986规范,由以下几部分组成。

  https://api.linianhui.test:8080/user/disabled?first_name=li#title
  \___/  \______________________/\_____________/\___________/\____/
    |               |                   |             |          |
  scheme        authority              path         query    fragment

URL的命名规则#URL命名规则

参考资料 :

  1. https://tools.ietf.org/html/rfc3986

3 HTTP

3.1 HTTP Method

面向资源设计的HTTP APIs中,绝大部分的操作都是CRUD(Create,Read,Update,Delete),都可以映射为某一个HTTP Method。其余的无法映射的操作一般存在两种解决方案 :

  1. 抽象出新的资源,比如禁用用户的操作。假设用户的资源是/user,那么可以抽象出来一个被锁定的用户的资源/user/disabled。如此以来,
    1. 禁用用户 : POST /user/disabled或者PUT /user/disabled/{user_id}
    2. 取消禁用 : DELETE /user/disabled/{user_id}
    3. 获取被禁用的用户列表 : GET /user/disabled
  2. 如果上面的方式无法满足需要,则可以采用POSTURL/动词的组合。还拿上面的举例 :
    1. 禁用用户 : POST /user/{user_id}/disable或者PUT /user/{user_id}/disable
    2. 取消禁用 : DELETE /user/{user_id}/disable
    3. 获取被禁用的用户列表 : GET /user?status=DISABLED

3.1.1 Names

HTTP Method NameSafeIdempotent描述说明
GET获取一个资源
PUT更新或创建一个资源(完整替换)
PATCH更新一个资源(部分更新)
DELETE删除一个资源
POST创建,或者不满足以上四个Method语义的所有操作

PATCHPOST都是不安全不幂等的,差异在于PATCH仅是用于部分更新资源, 而且是一个可选支持的HTTP Method,可能会存在一些代理、网关等组件不支持的情况,所以推荐用POST来代替它。

3.1.2 Semantics

每一个HTTP Method都具有一下3个HTTP协议层面的语义。

HTTP Method Semantics含义
Safe操作不会对资源产生副作用,不会修改资源。
Idempotent执行一次和重复执行N次,结果是一样的。
Cacheable可以被缓存。

参考资料 :

  1. https://tools.ietf.org/html/rfc7231#section-4
  2. https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods

3.2 HTTP Header

Http Header的用途在于携带HTTP RequestHTTP Response的元数据信息。

格式为Name:Value : Name不区分大小写,通常都采用首字母大写,-分隔的写法,比如Content-Type。按其用途可以分为如下4类 :

  1. 通用类 : 描述请求和响应的。
  2. 请求类 : 描述请求的。
  3. 响应类 : 描述响应的。
  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
LocationResponse中提供给客户端的连接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中,便于服务端追踪请求。

参考资料 :

  1. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
  2. https://tools.ietf.org/html/rfc7231#section-5
  3. https://tools.ietf.org/html/rfc7231#section-7

3.3 HTTP Stauts Code

HTTP Status Code用来指示HTTP协议层面的请求状态。它由一个数字和一个描述消息构成,比如200 OK。有以下几类状态码 :

  1. 2xx : 执行成功。
  2. 4xx : 客户端的错误,通常情况下客户端需要修改请求然后再次发送请求。
  3. 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服务器繁忙,暂时无法处理客户端的请求。

参考资料 :

  1. https://tools.ietf.org/html/rfc7231#section-6
  2. https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
  3. https://httpstatuses.com/

3.4 HTTP Error Response

虽然#HTTP Stauts Code4xx5xx的状态码来表示哪里出错了,但是其代表的只是HTTP协议层面的错误描述,它无法提供和业务相关的更具体错误描述。基于此种情况,我们需要设计一套描述业务层面错误的数据结构 :

{
  "method": "POST",
  "url": "/book",
  "statusCode": 400,
  "errors": [
    {
      "code": "name",
      "value": null,
      "message": "书名不能为空。"
    },
    {
      "code": "price",
      "value": -1,
      "message": "价格不能为负数"
    }
  ],
  "extra": null
}

以上这个数据结构仅在状态码为4xx5xx出现的时候才会使用;2xx的时候则不包含此数据结构。

code字段可以是一些出错的字段名、某一错误类别(比如NO_PERMISSION这种全局唯一的常量字符串)等等。

{
  "method": "DELETE",
  "url": "/book/1",
  "statusCode": 403,
  "errors": [
    {
      "code": "NO_PERMISSION",
      "value": "BOOK:DELETE",
      "message": "没有BOOK:DELETE的权限"
    }
  ],
  "extra": null
}

参考资料 :

  1. Problem Details for HTTP APIs : https://tools.ietf.org/html/rfc7807

4 JSON

JSON是一种应用非常广泛的数据交换格式。其包含6种基本的数据类型。

JSON数据类型示例
string"lnh"
number1.23
array[...]
object{...}
booltrue/false
nullnull
  1. JSON的命名规则#JSON命名规则
  2. JSON中没有原生的日期和时间类型,应该遵循#Date Time的要求,使用string类型表示。
  3. JSON中出现的和国际化相关的数据遵循#i18n中的要求。
  4. 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"
}

参考资料 :

  1. http://json.org/
  2. https://tools.ietf.org/html/rfc7159

5 版本化

Level 2的HTTP APIs中,虽然我们推荐也努力使得我们的APIs不做不兼容的改动,但是依然无法彻底的避免不兼容的升级。这就使得我们不得不对APIs进行版本化管理。通常有以下 3 种方案 :

  1. URL
    GET http://api.linianhui.com/v1/weather HTTP/1.1
    
  2. Request Header
    GET http://api.linianhui.com/weather HTTP/1.1
    Api-Version: v1
    
  3. 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组件命名规则
schemeall-lower-hyphen-case
authorityall-lower-hyphen-case
pathall-lower-hyphen-case
queryall_lower_underscore_case
fragmentall-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_nameall_lower_underscore_case
filed_value无要求
ENUM_FILED_VALUEALL_UPPER_UNDERSCORE_CASE

ENUM_FILED_VALUE用于表示枚举字段,用全大写和_分隔符,以示和普通的字符串进行区分。示例 :

{
  "first_name":"li",
  "lase_name":"nianhui",
  "gender":"MALE",
  "remark":"描述信息",
  "age":1234
}

参考资料 :

  1. https://support.google.com/webmasters/answer/76329
  2. https://stackoverflow.com/questions/5543490/json-naming-convention
  3. https://tools.ietf.org/html/rfc3986#section-2.4

7 Date Time

日期和时间采用RFC3339中定义的通用的格式。表示方法如下 :

格式组成部分示例
date-fullyear4位数的年份2018
date-month2位数的月份04
date-mday2位数的日期01
time-hour2位数的小时02
time-minute2位数的分钟08
time-second2位数的秒数59
time-secfrac秒的分数部分.256
time-numoffset+/- time-hour:time-minute+08:00
time-offsetZ/time-numoffsetZ+08:00
partial-timetime-hour:time-minute:time-second time-secfrac02:08:59.256
full-datedate-fullyear-date-month-date-mday2018-04-01
full-timepartial-time time-offset02:08:59.256+08:00
date-timefull-dateTfull-time2018-04-01T02:08:59.256Z+08:00

扩展 : date-fullyear-month(年月)可表示为date-fullyear-date-month,比如2018-04

  1. 日期和时间应该满足上面表格中定义的格式。
  2. 优先采用UTC时间(即Z+00:00)。即使没有跨时区的要求,也必须携带时区偏移信息,比如2018-04-01T02:08:59.256Z+08:00

参考资料 :

  1. Date and Time Formats - ISO 8601 : https://www.w3.org/TR/NOTE-datetime
  2. 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个字母)。示例 :

  1. 中国 : CN
  2. 美国 : US
  3. 其他 : https://en.wikipedia.org/wiki/ISO_3166-2#Current_codes

货币代码 : 采用Currency CodeISO 4217:2015版本(3个字母)。示例 :

  1. 人民币 : CNY
  2. 美元 : USD
  3. 其他 : https://en.wikipedia.org/wiki/ISO_4217#Active_codes

语言代码 : 采用Language CodeISO 639-1:2002版本(2个字母)。示例 :

  1. 中文 : zh
  2. 英文 : en
  3. 其他 : https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes

区域代码 : 由Language CodeCountry Code组合而成。示例 :

  1. zh-CN
  2. en-US

参考资料 :

  1. https://www.iso.org/iso-3166-country-codes.html
  2. https://www.iso.org/iso-4217-currency-codes.html
  3. 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-012018-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)论文。

  1. 英文版 : https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
  2. 中文版 : https://www.infoq.cn/article/web-based-apps-archit-design/

HTTP APIs 四大组成部分(HTTP, URI, MIME, JSON)

  1. HTTP/1.1 ( RFC7230-7235 ) : https://tools.ietf.org/html/rfc7230
  2. URI ( RFC 3986 ) : https://tools.ietf.org/html/rfc3986
  3. MIME ( RFC 2387 ) : https://tools.ietf.org/html/rfc2387
  4. JSON : http://json.org/

Hypermedia

  1. JSON Schema: http://json-schema.org/

URL模板

  1. URI Template ( RFC6570 ) : https://tools.ietf.org/html/rfc6570

彩蛋:NO SOAP!

NO SOAP!
上一篇 : [代码规范] C#规范指南