RESTful API的设计总结
在查阅了很多有关RESTful API的教程案例、设计方法、原则之后,总结成笔记:
摘要
-
REST指 表述(表征)性状态转移;
-
资源的URI遵循可寻址性原则,具有自描述性,形式上给人直观的关联特点;
- 一般以一级级的名词构成URI
- 可以使用GET/POST/PUT/DELETE 对应方法对应查询、添加、更新、删除的操作
- API的URI是基于HTTP的名词性短语,用来表示资源
- 服务端接收到请求后返回200状态码表示成功,客户端获取的状态码不是200时表示链路/流程上某个环节出问题
-
RESTful API 只维护资源状态而不维护客户端状态
对RESTful而言次请求都是全新的,只需要针对本次请求做出对应的响应,不需要记录这次请求的信息
一个简单的服务端响应格式示例:
{
"code": -32600,
"message": "Invalid Request",
"data": {}
}
说明:
- 这样是一条典型的响应文档,可以以JSON字符串形式封装
- code为0表示调用成功,其他为自定义的错误码
- message表示在API调用失败的情况下详细的错误信息,这个信息可以选择呈现给用户看,也可为空
- data表示服务端返回的数据,具体格式由服务端定义,API调用错误时数据为空
详细说明
设计原则
- 把所有起作用的数据都列出来
- 将所有的数据用一种层级关系表示
- 版本控制,可以在URL前加上一个版本号
- URI规范
- 不用大写
- 用"-“代替”_"
- 参数列表要编码(encoding)
- URI中的名词用复数形式
- URI避免层级过深,可以使用查询参数降低层级深度
- URI的设计不一定直接对应数据库表,可以是某些字段的抽象或者组合,依托于实体存在,如GET /user/1/addresses address可以是 country city street等字段的组合
一些补充
-
安全性和幂等性
- 具备安全性的操作不会改变资源状态,可以理解成只读;
- 具备幂等性的操作将会改变资源状态,但是复数次的操作对资源状态的改变效果等价
-
复杂查询
类型 示例 说明 过滤条件 ?type=1&age=16
允许一定冗余 /zoos/1
和/zoos?id=1
等效排序 ?sort=age,desc
投影 ?whitelist=id,name,email
分页 ?limit=10&offset=3
-
常用查询条件组标签化,增强复用性,降低维护成本
GET /trades?status=closed&sort=created,desc
可以简化成:
GET /trades#recently-closed
GET /trades/recently-closed
-
常用的三种请求体(body format)格式
- json字符串
POST /v1/animal HTTP/1.1 Host: api.example.org Accept: application/json Content-Type: application/json Content-Length: 24 { "name": "Gir", "animalType": "12" }
- 浏览器POST FORM表单格式
POST /login HTTP/1.1 Host: example.com Content-Length: 31 Accept: text/html Content-Type: application/x-www-form-urlencoded username=root&password=Zion0101
- 表单有文件上传时的格式
Content-Type: multipart/form-data; boundary=-----RANDOM_jDMUxq4Ot5 ----------------------------625409775409345732557243 Content-Disposition: form-data; name="file1"; filename="吃饭的艺术.txt" Content-Type: text/plain ------------------------------------ 浅谈吃饭的艺术 当大家看到这个题目,也许会说,吃饭谁不会啊... ------------------------------------
- json字符串
-
常见的响应的资源格式
常见常用的有:json、xml、pdf、excel等等,个人比较喜欢json,具备自描述的特性,占用的空间也相对较小 -
客户端请求资源时可以设置各种格式的偏好程度
Accept:application/xml;q=0.6,application/atom+xml;q=1.0
q表示偏好的权重值 -
响应不做多余的包装
返回的数据内容应简单易操作,过于复杂的数据包装会增加前端解析数据时的开销 -
HTTP几种方法操作后返回的数据格式
方法 URI 返回内容 GET /collection
返回资源对象的列表(数组) GET /collection/resource
返回单个资源对象 POST /collection
返回新生成的资源对象 PUT /collection/resource
返回完整的资源对象 DELETE /collection/resource
返回一个空文档 -
json格式的约定规则
- 时间以时间戳的长整型/浮点表示,交由客户端自行解析
- 不传入null字段
- 分页的情况用列表(数组)封装数据
{ "paging":{"limit":10,"offset":0,"total":729}, "data":[{},{},{}...] }
-
错误处理
- 发生了错误后不要给客户端返回2xx响应,客户端可能会缓存状态码表示成功的http请求
- 正确设置http状态码,不要自定义
- Response body 提供
- 错误的代码(日志/问题追查)
- 错误的描述文本(展示给用户)
对第3点进行补充
- 服务器端一般用异常表示 RESTful API 的错误
- 可能抛出的异常有两类:业务异常和非业务异常
业务异常由自己的业务代码抛出,表示一个用例的前置条件不满足、业务规则冲突等,比如参数校验不通过、权限校验失败。
非业务类异常表示不在预期内的问题,通常由类库、框架抛出,或由于自己的代码逻辑错误导致,比如数据库连接失败、空指针异常、除0错误等等。
业务类异常必须提供2种信息:
如果抛出该类异常,HTTP 响应状态码应该设成什么,这里认为应该是400/401/403;
异常的文本描述;
在Controller层使用统一的异常拦截器:
设置 HTTP 响应状态码:对业务类异常,用它指定的 HTTP code;对非业务类异常,统一500;
Response Body 的错误码:异常类名
Response Body 的错误描述:-
对业务类异常,用它指定的错误文本;
-
对非业务类异常,线上可以统一文案如“服务器端错误,请稍后再试”,开发或测试环境中用异常的 stacktrace,服务器端提供该行为的开关。
-
常用的HTTP状态码及表示
状态码 | 含义 | 解释 |
---|---|---|
200 | OK - [GET] | 服务器成功返回用户请求的数据,该操作是幂等的(Idempotent) |
201 | CREATED - [POST/PUT/PATCH] | 用户新建或修改数据成功 |
202 | Accepted - [*] | 表示一个请求已经进入后台排队(异步任务) |
204 | NO CONTENT - [DELETE] | 用户删除数据成功 |
400 | INVALID REQUEST - [POST/PUT/PATCH] | 用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的 |
401 | Unauthorized - [*] | 表示用户没有权限(令牌、用户名、密码错误) |
403 | Forbidden - [*] | 表示用户得到授权(与401错误区别),但是访问是被禁止的 |
404 | NOT FOUND - [*] | 用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的 |
406 | Not Acceptable - [GET] | 用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式) |
410 | Gone -[GET] | 用户请求的资源被永久删除,且不会再得到的 |
422 | Unprocesable entity - [POST/PUT/PATCH] | 当创建一个对象时,发生一个验证错误 |
500 | INTERNAL SERVER ERROR - [*] | 服务器发生错误,用户将无法判断发出的请求是否成功 |
- 其他类型资源:服务类型的资源
除了资源简单的CRUD,服务器端经常还会提供其他服务,这些服务无法直接用上面提到的URI映射。如:
- 按关键字搜索;
- 计算地球上两点间的距离;
- 批量向用户推送消息;
可以把这些服务看成资源,计算的结果是资源的presentation,按服务属性选择合适的HTTP方法。例如:GET /search?q=filter?category=file 搜索 GET /distance-calc?lats=47.480&lngs=-122.389&late=37.108&lnge=-122.448 POST /batch-publish-msg [{"from":0,"to":1,"text":"abc"},{},{}...]
-
异步任务
对耗时的异步任务,服务器端接受客户端传递的参数后,应返回创建成功的任务资源,其中包含了任务的执行状态。客户端可以轮训该任务获得最新的执行进度// 提交任务: POST /batch-publish-msg [{"from":0,"to":1,"text":"abc"},{},{}...] // 返回: {"taskId":3,"createBy":"Anonymous","status":"running"} GET /task/3 {"taskId":3,"createBy":"Anonymous","status":"success"}
如果任务的执行状态包括较多信息,可以把“执行状态”抽象成组合资源,客户端查询该状态资源了解任务的执行情况。
// 提交任务: POST /batch-publish-msg [{"from":0,"to":1,"text":"abc"},{},{}...] // 返回: {"taskId":3,"createBy":"Anonymous"} GET /task/3/status {"progress":"50%","total":18,"success":8,"fail":1}
-
URI失效
随着系统发展,总有一些API失效或者迁移,对失效的API,返回404 not found 或 410 gone;对迁移的API,返回 301 重定向 -
Hypermedia API 超媒体API
RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档
{"link":{ "rel": "collection https://www.example.com/zoos", "href": "https://api.example.com/zoos", "title":"List of zoos", "type": "application/vnd.yourformat+json" }}
文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。
Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。
{ "current_user_url":"https://api.github.com/user", "authorizations_url":"https://api.github.com/authorizations", // ... }
从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果
{ "message":"Requires authentication", "documentation_url":"https://developer.github.com/v3" }