REST API 设计最佳实践

/ 0评 / 0

一 前言

作为一名后端程序员,照着产品需求设计好了模型,设计好了关联关系,设计API时候问题来了:一旦 API 进入前端 APP 代码,或者是被你的顾客广泛使用的话,再来大改就非常麻烦了。比如说,如果 APP 版本 1.0 用了一个接口 A,这个接口 A 如果要进行大改,那么必须将 A 维持至所有用户升级过 APP 1.0 后。那么怎么样避免 API 发布之后大改呢?有没有一些提前可以注意到的设计准则可以帮我们避开 API 设计中的各种坑?

二 REST API 是什么

REST API 有一套 API 设计的准则,它规范了 API 设计的框架,使得服务间、程序员之间有一个通用的沟通语言。

三 REST API 内具体规定了什么

REST API 规范了 API 设计的两大核心原则

  1. API应该作用于 Resource(资源)上
  2. 对资源的操作应使用对应语义的几种操作,包括: GET, POST, PUT, PATCH, DELETE

什么是Resource(资源)

这里的资源是可操作的逻辑对象,允许调用者进行操作,比如用户注册,那么 API 类似于POST /users,资源即为 users。在很多情况下,API 中的资源与你的数据模型是一一对应的。当然也有例外情况,比如说你的数据库中存有用户,但是你现在想要让调用者可以创建“管理员”,那么 API 可能是POST /admins表中并没有 admins 这个表,可能 admin 是 Users 表中的一个属性,比如 role=admin
REST API中的资源一定需要是名词,即一定是一个实在存在的概念,比如 用户, 帐号, 车票等,或一个抽象的概念,比如 权限 等。
如果你需要提供一个创建某种资源的API接口,POST /indexesPOST /accountsPOST /docs等等。
对于资源的命名,建议统一命名为为英文的复数。比如说 users 而不是 user。更重要的是保持一致性,在所有地方用一样的复数。

什么是操作

一旦定义了资源,接下来需要定义允许调用者在这些资源上做什么操作。
比如说,以抢车票为例,我们可能允许调用者进行以下操作

以上可以总结出来REST的大致设计思路了。它由两部分组成,第一部分是 操作,第二部分是可操作的 资源。比如上文中的 GET /tickets,操作是 GET,可操作的资源是车票。
如果严格遵循了REST的设计准则,调用者也了解 REST 的准则的话,那么对于很多 API 调用,不用再参考互相写的文档了。如果需要调用一张车票的信息,调用者自然会知道应该用GET去查看一个车票资源的信息,即 GET /tickets/:ticketId,这样就极大降低了沟通成本和出错成本,提升效率。

如何在 API 中表示实体(数据库表)间关系

在后端设计中,有的资源逻辑上无法独立存在。比如说,有索引存在,用GET /indexes/index_abc/docs/1来表达获取索引 index_abc 中编号为 1 的文档。因此,对于所有资源需要依赖于另一个资源存在时,我们就按顺序在端点中将资源列出来。索引和文档的关系,可以有以下接口

如果一个资源可以独立于另一个资源存在,那么可以考虑直接提供子端点。比如说,如果一个宠物店主人和宠物信息分别都常常被同时调用,那么可以考虑

GET /owners/  - 获取所有主人信息
GET /owners/1/pets/ 获取 id 为 1 的主人的所有宠物

GET /pets/ - 获取所有宠物信息(宠物店所有宠物)

GET /pets/13 - 直接获取 id 为 13 的宠物

REST API中如何表示一个动作

有时候表达一些接口时,会发现REST的准则很难直接应用。比如用户登录POST /users/signin,这里的 signin是个动词。采用REST准则时可以考虑有三个选择

  1. 严格地遵循 REST 原则,找一个替代动词的名词。 signin 可以替换为login。或者,以 token 密钥的方式登录的话,可以改为 POST /users/token,创建一个 user token(也就是登录了)
  2. 在某些实在困难的地方,放弃严格的REST原则
  3. 参考一些成功的 REST API 并寻找类似的 API,参考他们的命名设计,如 github 的 API,较为规范,覆盖了很多 API 调用的情景,可以找到个类似的命名参考。

比如说,在 github 上,如果让你来设计加星这个操作,你会把端点被设计成什么样?
Github把加星端点设计为 PUT /gists/:id/star,把取消加星设计为 DELETE /gitsts/:id/star。这样就完美地遵循了 REST 名词作为资源的准则,把动词"加星"完美地用 PUT/DELETE 两个操作清晰地表达出来。

REST API 设计常见问题和建议

如何区分版本

比如说,如果在大致将 v1 开发完毕后,v1 前缀的 API 就应该稳定下来,所有的改动进入 v2。同时开始通知所有使用 v1 的用户,帮助他们平滑迁移到 v2。带有版本前缀的 API 示例如下

GET /v1/indexes/
GET /v1/indexes/abc/
POST /v1/indexes/

也可以用子域名表示

http://api.example.com/v1
http://apiv1.example.com

或者添加自定义请求头部

Accept-version: v1
Accept-version: v2

过滤、排序、字段选择和分页

过滤:为所有字段或者查询语句提供独立的查询参数。

GET /cars?color=red 返回红色车的列表
GET /cars?seats<=2 返回两座车的列表

排序:允许跨越多字段的正序或者倒序排列。

GET /cars?sort=-manufactorer,+model 返回排序的车辆列表

字段选择:只列出需要的字段。一些情况下,只需要在列表中查询几个有标识意义的字段,并不需要从服务端把所有字段的值都请求出来,因此需要API支持选择查询字段的能力。这样也可以在一定程度上提高网络传输的性能与响应速度。

GET /cars?fields=manufacturer,model,id,color 返回需要的车辆信息字段的列表

分页:使用offset和limit来获取固定数量的资源结果,当其中一个参数没有出现时,应该提供各自的默认值,比如默认取第一页,或者默认取20条数据。

GET /cars?offset=10&limit=5 返回第10页的5条车辆记录列表
GET /cars?&limit=5 返回前5条车辆记录列表
GET /cars?&offset=5 返回第5页的车辆记录列表(默认单页数量)

应该返回什么

建议 REST API 永远返回 JSON 格式的结果。
因为这样才能返回标准的结构化数据。所以,服务器回应的 HTTP 头的Content-Type属性要设为application/json。客户端请求时,也要明确告诉服务器,可以接受 JSON 格式,即请求的 HTTP 头的ACCEPT属性也要设成application/json

HTTP 状态码

这五大类总共包含100多种状态码,覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。
2xx 状态码
200状态码表示操作成功,但是不同的方法可以返回更精确的状态码。

GET: 200 OK
POST: 201 Created
PUT: 200 OK
PATCH: 200 OK
DELETE: 204 No Content

上面代码中,POST返回201状态码,表示生成了新的资源;DELETE返回204状态码,表示资源已经不存在。
此外,202 Accepted状态码表示服务器已经收到请求,但还未进行处理,会在未来再处理,通常用于异步操作。

参考

  1. https://stackoverflow.blog/2020/03/02/best-practices-for-rest-api-design/
  2. https://hackernoon.com/restful-api-designing-guidelines-the-best-practices-60e1d954e7c9
  3. https://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
  4. Azure ( https://docs.microsoft.com/zh-cn/azure/architecture/best-practices/api-design
  5. Google Cloud ( https://cloud.google.com/apis/design/
  6. Zalendo : https://opensource.zalando.com/restful-api-guidelines/ ,其中有非常多的实践是可以参考的,也像 RFC 一样规范了 MUST 、SHOULD 、MAY 的遵守分级。