RESTful Best Practices
tags: RESTful Specification Apis Design
Author: Andy Ai
Weibo: NinetyH
声明
此文为实践总结,是自己在实践过程中积累的经验和"哲学"。部分内容参考相关资料,参考内容请看尾页。建议对RESTful有一定了解者阅读!
哲学
不要为了RESTful而RESTful
在能表达清楚的情况下,简单就是美
接口路径设计
接口设计原则
URI指向的是唯一的资源对象
示例: 指向ID为yanbo.ai
的Account
对象
GET http://~/$version/accounts/yanbo.ai
URI可以隐式指向唯一的集合列表
示例: 隐式地指向trades list 集合
GET http://~/$version/trades/(list)
等同于
GET http://~/$version/trades
聚合资源必须通过父级资源操作
示例: Profile
是User
的聚合资源,User
有一个唯一且私有的Profile
资源,只能通过User
操作Profile
。
更新user_id为123456的Profile资源
PUT http://~/$version/users/123456/profiles
Request Body:
{
"full_name": "yanbo.ai",
"state": "Shanghai",
"title": "Senior software engineer"
}
组合资源要避免资源路径嵌套
示例: 一个系统里面包含多个 applications,一个 application 又包含多个 users。那获取 user 资源的路径应该是怎样的?
看一个路径嵌套的例子:
GET http://~/$version/systems/:systemId/applications/:applicationId/users/:userId
这样做是不合理的,它会让你的接口变得越来越混乱和缺少灵活性。正确的做法是:
GET http://~/$version/systems/:systemId
GET http://~/$version/applications/:applicationId
GET http://~/$version/users/:userId/
Http Methods
HTTP Operation | Description |
---|---|
GET | 获取,查找 |
POST | 新增创建 |
PUT | 更新 |
PATCH | 部分更新 |
DELETE | 删除 |
URL组成
网络协议(HTTP, HTTPS)
服务器地址
版本
接口名称
?参数列表
GET https://github.com/v1/trades
为什么需要版本?
当服务被更多其他系统使用的时候,服务的可用性和上下兼容变得至关重要。被外部系统依赖的服务在升级时是一个非常麻烦的事情,既要发布新的接口,又要保留旧的接口留出时间让调用者去升级。在URL中加入Version
标示能很好地解决上下兼容(新老版本共存)问题。
示例1: URL中新增了Path parameter
v1版本
GET http://~/v1/trades?user_id=123456
v2版本
GET http://~/v2/:user_id/trades
示例1中的user_id
参数在v2版本被加入到path parameter中,使用$version
保证了v1
和v2
接口的共存。
示例2: 数据接口发生变化
v1版本
GET http://~/v1/accounts/yanbo.ai
Response Body:
{
"user_name": "yanbo.ai",
"e_mail": "yanbo.ai@gmail.com",
"state": "Shanghai",
"title": "Senior software engineer"
}
v2版本
GET http://~/v2/accounts/yanbo.ai
Response Body:
{
"user_name": "yanbo.ai",
"e_mail": "yanbo.ai@gmail.com",
"profile": {
"state": "Shanghai",
"title": "Senior software engineer"
}
}
示例2中的接口返回数据结构已经发生了变化。使用$version
保证了v1
和v2
接口的共存。
URL定义限制
不使用大写字母
使用中线
-
代替下划线_
参数列表应该被encode过
接口分类
资源对象的CURD操作
GET http://~/$version/trades 获取trades列表
GET http://~/$version/trades/:id 根据id获取单个trade
POST http://~/$version/trades 创建trade
PUT http://~/$version/trades/:id 根据id更新trade
PATCH http://~/$version/trades/:id 根据id部分更新trade
DELETE http://~/$version/trades/:id 根据id删除trade
服务型接口
使用services
标识,根据服务的属性选择http方法。
http://~/services/$version/server-name
系统设置
使用settings
标识,根据服务的属性选择http方法。
http://~/settings/$version/server-name
示例1: 搜索
GET http://~/services/$version/search?q=filter?category=file
示例2: 任务队列操作
PUT http://~/services/$version/queued/jobs 往任务队列里面添加一个新的任务
DELETE http://~/services/$version/queued/jobs/:id 根据id删除任务
示例3: 更改界面语言环境
PUT http://~/settings/$version/gui/lang
{
"lang": "zh-CN"
}
为什么需要区分?
-
`Microservices`是一个全新的概念,它主要的观点是将一个大型的服务系统分解成多个微型系统。每个微型系统都能独立工作,并且提供各种不同的服务。独立运行的特点使微型系统之间不会产生相互影响,其中的一个微型系统宕机并不会牵连到其他的微型系统。这种架构使[分布式系统的节点数量][6]大大提升。因为RESTful服务是无状态的,所以这种分解并不会带来状态共享的问题。
-
路由规则(逻辑)
当我们需要对不同属性的接口做路由规则的时候,按功能划分接口是一个很好的方案。例如:我们要对系统设置接口设置增加更严格的调用限制。
缓存
网络接口相对于堆栈接口来说数据传输极其不稳定,尽可能地减少数据传输不仅能控制这种风险还能减少流量。使用缓存还能有效地提高后台的吞吐量。
后台在响应请求时使用响应头E-Tag
或Last-Modified
来标记数据的版本,前台在发送请求时将数据版本通过请求头If-None-Match
帮助后台判断缓存的使用。
Request Header
If-None-Match: 2390239059405940
Response Header
E-Tag: 2390239059405940
Last-Modified: 2014-04-05T14:30Z
Bookmarker
在实际的环境中,有大量的查询需求是相同的。将这些搜索需求标签化能降低使用难度也可以达到重用的目的。
示例1: 查找状态为关闭的订单
普通方式
GET http://~/$version/trades?status=closed&sorting=-created_at
Bookmarker
GET http://~/$version/trades#recently_closed
或
GET http://~/$version/trades/recently_closed
HATEOAS
HATEOAS通过Web Linking的方式来描述程序的状态信息
Link 主要包含以下属性:
Property | Description |
---|---|
rel | 关联内容 |
href | URL |
type | 媒体类型 |
method | Http Method |
title | 标题 |
arguments | 参数列表 |
value | 返回值 |
Rel
可能为以下值:
Value | Description |
---|---|
next | 下一步 |
prev | 上一步 |
first | 第一步,最前 |
last | 最后一步,最后 |
source | 来源 |
self | 资源自身,相对于this |
Web Linking 可以通过两种方式传递至客户端:
Http Header
Link: <http://~/$version/trades?page_no=10>; rel="next", <http://~/$version/trades?page_no=19>; rel="last"
Http JSON Body
{
"links": [
{
"rel": "next",
"href": "http://~/$version/trades?page_no=1"
},
{
"rel": "last",
"href": "http://~/$version/trades?page_no=19"
}
]
}
示例1: 用户注册业务
用户填写E-Mail与密码
完善用户资料
Register Request
POST http://~/$version/accounts
Headers:
Accept: application/json
Content-Type: application/json;charset=utf-8
Body:
{
"username": "yanbo.ai@gmail.com",
"e_mail": "yanbo.ai@gmail.com",
"password": "balabala"
}
Register Response
Headers:
Content-Type: application/json;charset=utf-8
Status: 201 Created
Body:
{
"uri": "http://~/$version/accounts/yanbo.ai",
"identity": "yanbo.ai",
"created_at": "2014-04-05T14:30Z",
"links": [
{
"rel": "next",
"href": "http://~/$version/accounts/yanbo.ai/profiles",
"method": "POST",
"title": "Editing Profiles",
"arguments": "status=editing"
}
]
}
Profile Request
POST http://~/$version/accounts/yanbo.ai/profiles
Headers:
Accept: application/json
Content-Type: application/json;charset=utf-8
Body:
{
"full_name": "yanbo.ai",
"state": "Shanghai",
"title": "Senior software engineer"
}
Profile Response
Headers:
Content-Type: application/json;charset=utf-8
Status: 201 Created
Body:
{
"uri": "http://~/$version/accounts/yanbo.ai/profiles",
"identity": "yanbo.ai",
"created_at": "2014-04-05T14:30Z"
}
示例2: 请看下节<分页>
HATEOAS在解决什么问题?
HATEOAS是Hypermedia as the Engine of Application State的缩写形式,中文意思为:超媒体应用状态引擎。它的核心思想是使用超媒体表达应用状态,与hypertext-driven思想是一致的。在此之前,我们大多数的程序业务控制在前台完成。例如:我们会在前台做注册流程,我们在前台判定下一步应该做什么,可以做什么。当使用HATEOAS时,这些状态流程控制都在应用程序的后台完成。我们使用超媒体来表达前台做完某一步骤之后可以做哪些? 这样一来,前台的任务就变得相当简单了,前台需要处理的是理解状态表述,数据收集和结果显示。
思考
HATEOAS会带来怎样的改变? 使用它的意义在哪?
分页
Request
GET http://~/$version/trades?page=10&pre_page=100
Response
Link Header
Link: <http://~/$version/trades?page=11&pre_page=100>; rel="next", <http://~/$version/trades?page=19&pre_page=100>; rel="last"
JSON Body
{
"links": [
{
"rel": "next",
"href": "http://~/$version/trades?page=11&pre_page=100"
},
{
"rel": "last",
"href": "http://~/$version/trades?page=19&pre_page=100"
}
]
}
安全
调用限制
为保证服务的可用性应对服务进行调用过载保护
Response Headers
X-RateLimit-Limit: 3000 调用量的最大限制
X-RateLimit-Reset: 1403162176516 调用限制重置时间
X-RateLimit-Remaining: 299 剩余的调用量
安全验证
RESTful服务使用Oauth2的方式进行调用授权,使用http请求头Authorization
设置授权码; 必须使用User-Agent
设置客户端信息, 无User-Agent
请求头的请求应该被拒绝访问。
Request Header
User-Agent: Data-Server-Client
Authorzation: Bearer 383w9JKJLJFw4ewpie2wefmjdlJLDJF
为什么建议使用Oauth2授权?
Oauth2的参与者为:客户端,资源所有者,授权服务器,资源服务器。客户端先从资源所有者得到授权码之后使用授权码从授权服务器得到token
,再使用token
调用资源服务器获取经过资源所有者授权使用的资源。这种授权方式的特点有:
资源所有者可以随时撤销授权许可
可以通过撤销
token
拒绝客户端的调用资源服务器可以拒绝客户端的调用
通过这三种方式可以做到对资源的严格保护。资源的访问权限也把握在资源所有者的手中,而不是资源服务器。
当然,Oauth2授权框架也允许受信任的客户端直接使用token
调用资源服务器获取资源。这种灵活性完全取决于客户端类型和对资源的保护程度。
为什么授权码要放在Http Header中?
WEB服务器对访问做记录已经成为了行业的一个标准,访问记录不仅可以用来做访问量统计还能用来做访问特征分析。互联网广告平台就是利用访问记录来做精准营销的。如果
token
(授权码)包含在URL中就有很大的安全风险。包含在URL中的
token
串可能被进行重定向传递。通过这两种方式入侵者可以不通过授权而使用泄漏的授权码访问那些受保护的数据,会造成数据泄漏的风险。
以Tomcat为例,访问日志为:
127.0.0.1 - - [24/Jun/2014:14:38:04 +0800] "GET /v1/accounts/yanbo.ai?token=dgdreLJLJLER798989erJKJK HTTPS/1.1" 200 343
通过对访问日志的提取,很容易得到token
信息。
数据设计
交互原则
查询,过滤条件使用query string。
用来描述数据或者请求的元数据放Header中,例如
X-Result-Fields
。Content body 仅仅用来传输数据。
数据要做到拿来就可用的原则,不需要“拆箱”的过程。
使用ISO-8601格式表达时间字段,例如:
2014-04-05T14:30Z
。
结构
使用JSON格式传输数据,在http请求头和响应头申明Content-Type
。返回的数据结构应该做到尽可能简单,不要过于包装。响应状态应该包含在响应头中!
Request
Accept: application/json
Content-Type: application/json;charset=UTF-8
Response
Content-Type: application/json;charset=UTF-8
错误的做法
{
"status": 200,
"data": {
"trade_id": 1234,
"trade_name": "Bala bala"
}
}
正确的做法
Response Headers:
Status: 200
Response Body:
{
"trade_id": 1234,
"trade_name": "Bala bala"
}
示例1: 创建User
对象
POST http://~/$version/users
Request
headers:
Accept: application/json
Content-Type: application/json;charset=UTF-8
body:
{
"user_name": "Andy Ai"
}
Response
status: 201 Created
headers:
Content-Type: application/json;charset=UTF-8
body:
{
"uri": "http://~/$version/users/1234",
"identity": 1234,
"created_at": "2014-04-05T14:30Z",
"links": [
{
"rel": "next",
"href": "http://~/gui/users/1234"
}
]
}
为什么是JSON?
JSON
是一种可以跨平台高扩展的轻量级的数据交换格式。易于人阅读和编写,同时也易于机器解析和生成。
属性定义限制
不能使用大写(大小写友好)
使用下划线_命名(连接两个单词)
属性和字符串值必须使用双引号""
提取部分字段
无状态服务器应该允许客户端对数据按需提取。在请求头使用X-Result-Fields
指定数据返回的字段集合。
例如:trade 有trade_id
, trade_name
, created_at
三个属性,客户端只需其中的trade_id
与trade_name
属性。
Request Header
X-Result-Fields: trade_id,trade_name
子对象描述
数据里面的子对象使用URI描述不应该被提取,除非用户指定需要提取子对象
示例: trade
里面的order
对象
错误的做法
{
"trade_id": "123456789",
"full_path": null,
"order": {
"order_id": "987654321"
}
}
正确的做法
{
"trade_id": "123456789",
"order": "http://~/$version/orders/987654321"
}
应用指定提取子对象,需要在请求头声明X-Expansion-Fields
Request
X-Expansion-Fields: true
为什么要客户端指定提取子对象时才提取?
懒模式服务能够最大程度地节省运算资源。虽然与客户端交互的次数有所增加,但是能做到按需提取,按需响应,这也是响应式设计的一大特点。客户端的用户行为模式无法真实地模拟,也就无法确定哪些资源需要做到一次性推送,让客户端按需使用是一个不错的方式。
关于空字段
应该在返回结果里面剔除空字段,因为null值传输到客户端并没有实际的含义,反而增加了占用空间。
Tips
使用HTTP Header时,优先使用合适的标准头属性。用X-
作为前缀自定义一个头属性,例如: X-Result-Fields
。
状态码&错误处理
应用状态码
Code | HTTP Operation | Body Contents | Description |
---|---|---|---|
102 Processing | GET, POST, PUT, DELETE, PATCH | 处理状态的信息 | 当前请求正在处理 |
200 Ok | GET, PUT | 资源 | 操作成功 |
201 Created | POST, PUT | 资源, 元数据 | 对象创建成功 |
202 Accepted | POST, PUT, DELETE, PATCH | 处理信息 | 请求已经被接受 |
204 No Content | DELETE, PUT, PATCH | N/A | 操作已经执行成功,但是没有返回数据 |
301 Moved Permanently | GET | link | 资源已被移除 |
303 See Other | GET | link | 重定向 |
304 Not Modified | GET | N/A | 资源没有被修改 |
400 Bad Request | GET, POST, PUT, DELETE, PATCH | 错误提示 | 参数列表错误(缺少,格式不匹配) |
401 Unauthorized | GET, POST, PUT, DELETE, PATCH | 错误提示 | 未授权 |
403 Forbidden | GET, POST, PUT, DELETE, PATCH | 错误提示 | 访问受限,授权过期 |
404 Not Found | GET, POST, PUT, DELETE, PATCH | 错误提示 | 资源,服务未找到 |
405 Method Not Allowed | GET, POST, PUT, DELETE, PATCH | 错误提示 | 不允许的http方法 |
406 Not Acceptable | GET, POST, PUT, DELETE, PATCH | 错误提示 | 媒体内容不符合要求 |
408 Request Timeout | GET, POST, PUT, DELETE, PATCH | 错误提示 | 请求超时 |
409 Conflict | GET, POST, PUT | 错误提示 | 资源冲突,重复的资源 |
415 Unsupported Media Type | GET, POST, PUT, DELETE, PATCH | 错误提示 | 不支持的数据(媒体)类型 |
422 Unprocessable Entity | GET, POST, PUT, PATCH | 错误提示 | 请求格式正确,但是由于含有语义错误,无法响应。 |
423 Locked | GET, POST, PUT, DELETE, PATCH | 错误提示 | 当前资源被锁定 |
429 Too Many Requests | GET, POST, PUT, DELETE, PATCH | 错误提示 | 请求过多被限制 |
500 Internal Server Error | GET, POST, PUT, DELETE, PATCH | 错误提示 | 系统内部错误 |
501 Not Implemented | GET, POST, PUT, DELETE, PATCH | 错误提示 | 接口未实现 |
容器状态码
容器状态码是指http容器的状态码,应用不应该使用或限制使用
Code | HTTP Operation | Body Contents | Description |
---|---|---|---|
303 | GET | link | 静态资源被移除,应用限制使用 |
503 | GET, POST, PUT, DELETE, PATCH | text body | 服务器宕机 |
Tips
4开头的错误用来表达来自于客户端的错误,例如: 未授权,参数缺失。5开头的错误用来表达服务端的错误,例如: 在连接外部系统(DB)发生的IO错误。
错误信息格式
错误信息应该包含下列内容:
错误标题
message
, 必须错误代码
error code
, 必须错误信息
error message
, 必须资源
resource
, 可选属性
field
, 可选文档地址
document
, 可选
Tips
Error Code
尽可能做到简洁明了,提取异常的关键字并且使用下划线_把它们连接起来。
示例: 调用频率超过限制,Response:
Headers:
Content-Type: application/json;charset=UTF-8
X-RateLimit-Limit: 3000
X-RateLimit-Reset: 1403162176516
X-RateLimit-Remaining: 0
{
"message": "Message title",
"errors": [
{
"code": "rate_limit_exceeded",
"message": "Too Many Requests. API rate limit exceeded",
"document": "https://developer.github.com/v3/gists/"
}
]
}
锦上添花
格式化(Pettyprint)JSON数据(返回结果)并且使用gzip压缩,Pettyprint易于阅读,多余的空格在经过gzip压缩之后占用空间比压缩之前更小。
重写
Server
头返回
X-Powered-By
Response Headers
X-Pretty-Print: true
Content-Encoding: gzip
Server: ods@shuyun.com
X-Powered-By: yanbo.ai;email=yanbo.ai@gmail.com
附页
框架&工具
参考资料
http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
https://developer.yahoo.com/social/rest_api_guide/http-response-codes.html
http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
未经同意不可转载, 转载需保留原文链接与作者署名。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。