分布式API设计 下:REST
2000年,互联网兴起数年,企业系统广泛流行通过HTTP 实现远程调用,代替EJB,当时基于HTTP远程调用,无论URL的设计,参数命名,以及响应都没有统一的规范。Roy Fielding 针对此问题, 提出了表示状态转移(Representational State Transfer),简称REST,作为一种设计web服务的体系结构方法 .REST 是目前最流行的HTTP API 设计规范,用于 Web 接口的设计 。
REST的核心思想就是,客户端发出的指令都是"标准动词 + 操作资源"的结构。比如,GET /orders
/1这个命令,GET
是动词,/orders
/1 是宾语,这里的1是指订单编号为1的订单。
如下是一个REST请求
GET https://xxxx/orders/1 HTTP/1.1
Accept: application/json
响应
HTTP/1.1 200 OK
{
"code":"SUCCESS"
"messsage":"成功"
"data":{"orderId":1,"orderValue":99.90,"productId":1,"quantity":1}
}
这里的标准动词是HTTP规范提供的标准,如上面的get,其他动作还有post,delete,将会在后面描述。
相比于HTTP 远程调用,REST是以资源作中心构建的远程调用服务,这里的资源指的是业务实体,业务实体来自于数据架构。一个良好的REST API设计,离不开良好的数据架构
以GIT 提供的服务为例子,资源包括了
- 仓库
- 用户
- Issues
- Release
- Team
以物联网为例子,资源包括了
- 设备
- 用户
- 家庭
- 属性
- 告警
- OTA
- 命令
在上一节,比较过二进制协议和HTTP的区别,建立在HTTP之上的REST,具备HTTP的优点
- 语言独立: 所有语言都支持HTTP协议,也提供REST风格的客户端接口
- 平台独立: 所有的系统都支持HTTP
- 可观测性强: 通过HTTP代理服务器,以及良好的REST接口定义,可以让系统之间调用可观测,可统计
基于HTTP协议的除了REST外,还有非常流行的SOAP,简单区分俩种的区别
- REST更加轻量级,SOAP更加重量级,SOAP更像是传统分布式调用体系的的HTTP版本,适合传统企业应用,适合系统之间调用。比如SOAP严格定义了自己的安全机制,寻址方式,以及协议格式。而REST则不受限制。
- REST以资源为中心,SOAP通常是以服务为中心。
本节介绍REST的设计最佳实践,它除了上一节的分布式API通用的最佳实践外,REST API 还有 特定的设计原则,总结为下表内容和表后的重点说明
设计原则 | 描述 |
---|---|
URL | URL必须定义了资源,比如/books/1978 |
命名规范 | 驼峰命名方法,蛇形命名法,烤肉串命名法。比如对于显示名称 display name。分别对应了displayName,display_name, dispaly-name。 REST的URL通常使用烤肉串命名法,入参和出参数使用Java常用的驼峰命名法 |
资源名称 | API的核心是操作是WEB资源或者业务实体,因此需要在这之前定义好这些名称,这些名称在数据架构或者更早的的数据架构前已经定好。比如用户中心提供的REST接口查询用户的绑定设备GET /users/devices 。如果缺少标准名称,另外一个接口查询设备状态 GET /devs/customer . 这让API使用者有困惑,他可能认为devices和devs 可能是物联网的俩种设备资源名称通常以复数形式体现 |
资源层级 | 当资源必须通过层级获取的时候,可以用/表达层级,比如/orders/{orderId}/items, 建议层级不超过3层。 |
标准字段 | 定义常见的标准字段,如requestId,token,createTime,orderBy,displayName,startDate,endDate等. 其他行业特定的字段应该基于数据架构的概念模型来定义,比如物联网中的device,shadow, 电商中的coupon,order,sku |
开闭区间 | 表示范围,需要统一规定是否包含临界值,Java通常使用[、),起始值闭区间,结束值为开区间,推荐使用这种约定 |
使用单位后缀 | 比如优先使用maxByte代替max,除非有上下文指示返回的单位。 关于时间,精确到秒,应该使用time,精确到天,应该使用date后缀。 |
名字缩写 | 软件开发者常用的缩写可以用在API上,如id,config,电商中的Sku。 |
保留字 | class是java的关键字,因此在设计API的时候需要避免参数中使用此关键字,这主要考虑到序列化和反序列化时候,这类关键字无法被实现。 |
版本号 | 通常使用version表示api的版本,比如/gms/v1.0/devices,建议REST URL 总是包含version。另外可选的是在HTTP Header中包含version。比如github api推荐把version放到header里 |
下表列出了 Github REST API
REST接口 | 说明 |
---|---|
/user/repos | 获取当前用户的所有仓库 |
/repos/{owner}/ | 查询一个仓库信息 |
/repos/{owner}/{repo}/releases | 来自github api,返回代码库的所有release |
/repos/{owner}/{repo}/releases/ | 来自github api,返回代码库的指定的release 信息 |
下列情况是错误的URL例子
REST接口 | 正确 | 错误分析 |
---|---|---|
/book | /books | 通常使用复数,除非查询单个对象 |
/user/create | /user | URL 不应该包含动词 |
/users/{id}/books.json | /users/{id}/books | 无需指定返回的格式,通常默认是json,如果需要其他格式,可以通过媒体类型来指定,比如application/xml |
思考:查看你系统的的REST API,看看有没有不符合RESTAPI 设计原则的
标准方法命名
HTTP提供了标准的动作 GET,PUT,POST,DELETE等,
- GET:读取
- POST:新建
- PUT:创建或者更新
- PATCH:更新(Update),通常是部分更新
- DELETE:删除(Delete)
URL | 动作 | 含义 |
---|---|---|
/users | GET | 查询所有用户 |
/users/1 | GET | 查询id为1的用户 |
/users | POST | 创建新的用户 |
/users/1 | PUT | 创建或者更新用户,id为1 |
/users/1 | DELETE | 删除用户1 |
/users | DELETE | 删除所有用户 |
范围查询
规定使用offset和limt作为查询参数,如果数据来源是大数据,通常使用cusor方式查询,翻页范围使用cusor和limit参数俩个,这里的cusor通常是递增主键id
HTTP Stauts
在开发API时候,对于REST的响应结果,还需要进一步规范如下API说明,通常用如下
- 200 表示成功
- 400 表示客户端错误
- 400 服务器不理解请求,如参数错误
- 401 未通过身份验证
- 403 未授权的访问资源
- 404 访问资源不存在
- 500 表示服务端错误
关于HTTP Status 定义,参考HTTP 响应状态
如果这些Status Code不足以表达更明确的含义,建议在响应体的JSON里包含code 进一步明确错误含义,比如code为PARAMETER_ERROR 表示参数校验错误。message通常用于补充code含义
HTTP/1.1 200 OK
Content-Type: application/json
{
"code":"SUCCESS"
"messsage":"库存查询成功"
"data":{
}
}
错误响应: 对于基于HTTP协议的API,可以充分利用HTTP STATUS来表示API是否成功,STATUS 304 表示授权错,STATUS 500 表示内部失败,等等,使用HTTP STATUS也有利于其他系统,如网关或者监控系统对错误进行观测。
HTTP/1.1 500 ?
Content-Type: application/json
{
"code":"PARAMETER_ERROR"
"messsage":"操作失败,"
"data":{
}
}
使用HTTP Status,还是使用响应的Body中的code来指示REST的响应结果,屈居于俩个因素
1) HTTP Status仍然能粗略的表达REST 响应状态,屈居于客户端是否对状态更详细的要求,比如HTTP 500表示服务器内不错误,对于大多数电商和企业应用系统,是不足以表达的
1) 可观测性,如果已经搭建好的有可观测系统,且REST返回结果能被客观性系统收集到,则可以使用CODE来指示结果。 如果此时搭建的有NGINX 的日志监控,HTTP Status也能粗略的统计到REST 请求结果
如果API实现异步调用,需要通过文档向调用者说明,从哪里可以获取异步执行结果。或者提供一个新的API,用户可以通过reqeust_id,查询异步调用结果。或者服务端返回一个查询ID,用户可以用此ID查询结果
或者对于REST请求,也可以在响应中包含一个URL,供用户查询。如下一个成功响应
HTTP/1.1 200 OK
Content-Type: application/json
{
"code":"ASYNC-SUCCESS"
"messsage":"成功"
"data":{
"url":"/device/ECD0017R1/status/344fer55656563"
}
}
无论是成功或者失败,都应该包含code和message,code是成功或者错误的CODE定义,message是用户可阅读的的消息。
对于响应的JSON,还有如下设计建议
- 需要明确的区分其属性是否为null,或者不存在
- 返回的空集合,使用[],而不是null,或者不存在
- 返回的浮点数或者长整形,如果客户端是JS,需要注意到有可能JS不支持,这情况下改成字符串
总有例外
本节包含了通常REST API 最佳实践,有其他资料对API 最佳实践有更严格的规定,架构师需要衡量是否采用这些严格规定, 本文列举了几个需要衡量的点
- 充分使用HTTP Status Code,有些REST规范要求使用更多的HTTP Status Code表示,比如对于客户端错误,采用403表示权限不足,,404表示访问资源不存在使用。参考rfc9110 了解更多status含义。本书建议使用200,400,以及500即可,更详细的错误信息放在code里
- REST API的GET查询不推荐携带Body。允许GET with Body,即允许GET查询携带Body。这是因为可能查询参数结构较为复杂,比如ES提供的/_search, 其body就是一个JSON格式。再比如,通常客户端JS框架会把页面查询条件的表单序列化成JSON。因此通常建议使用POST请求,且URL路径上增加查询动作,比如/books/search
- 删除通常是用DELETE,如果是按照条件删除,可以使用POST,比如ES中的按照查询条件删除 POST /my-index-000001/_delete_by_query
- REST API 大部分实践都要求参数是蛇行命名。本书建议,蛇行命名,以可以像Java那样驼峰命名
通常,基于GET请求的REST,意味着 通过URL分享此REST接口,其结果易于被客户端或者代理缓存,支持重定向等功能,如果GET支持BODY,那必须确认HTTP 代理或者是WEB 框架是否支持。
参考:
- https://docs.github.com/en/rest
- 阮一峰:RESTful API 最佳实践
- Zalando RESTful API and Event Guidelines
- 微软 RESTful web API design
- Github API
- 博客 API Design Patterns for REST