类似于阿里巴巴的开发规范或者谷歌的开发规范,API设计是目前很多场景下的基本功,所以这里给出一个笔者的最佳实践。
一、引言
REST,即Representational State Transfer的缩写,关于RESTful架构,可以参考《架构之美》中的定义。
- 客户端和服务器之间的交互在请求之间是无状态的,每个请求都必须包含理解请求的全部信息。
- 在此基础上,服务更容易实现分布式、水平扩展、异步处理和可重入(幂等请求)。
REST架构中的基本定义
- 资源(Resource): 网络上的所有内容都表述成一个资源、一个实体或者一个具体的信息,它可以是一段文本、一张图片、一首歌曲或者一种服务。
- 统一资源定位符(URI, Universal Resource Identifier): 一个资源的识别符或者说是一个地址,通过URI可以定位到一个唯一的资源,网络上每个资源都有一个唯一的标识符。
- 状态转换(State Transfer): 所有资源都共享统一的接口,以便在客户端和服务器之间传输状态。客户端和服务器端互动的过程,通常涉及到服务器端数据和状态变化的过程,比如文件被修改、访问数量增加等。
- 使用基本的HTTP请求方法。
GET: 获取资源
POST: 更新资源
PUT: 更新资源
DELETE: 删除资源
- Hypermedia: 应用程序状态的引擎,实现hypermedia是API的最佳实践之一。
二、基本的设计原则
1. 协议(Protocol)
API与客户端通信的协议,应该尽量使用HTTPS。除此之外,白名单机制、VPN可以提高更高的安全性。遵循“所有人应该知道他所需要完成工作必备的最少的知识”的原则。而一个接口,只有在访问者必须使用它时,才告诉这个访问者。
2. 域名(Domain)
应该将API部署到专用域名之下,好处显而易见。
- 更容易做动静分离。
- 更容易做服务降级和限流。
- 更容易做到高吞吐量。
- 更容易做流量分发。
- 更容易进行之后的水平扩展和服务拆分。
示例:https://api.groupname.domain.io/
更好的实践是将服务分组,并且根据情况进行必要的层次和分组(group)。这里的group可以包含事业部,也可以包含不同的客户,更可以包含不同的层次。
规范:https://api.[service-group].domain.io/
3. 基本URL(Root URL)
基本的URL,在此文中指的是除了服务拆分之外的URL,这里的最佳实践是可以包含环境、版本、分类、层次等,但是一个基本的URL,已经能决定除了接口或者服务粒度以外的所有事情,或者说可以决定由一个作战单元(个人或者小的敏捷团队)日常维护的工作内容了。
示例:https://api-dev.groupname.domain.io/mobile/v1/comment/[...]
- 此处dev表示开发环境(这里的api默认表示生产环境,api-dev表示开发环境,职责仍然是唯一的)。
- groupname表示分组(可以包含更多层次信息或者更多分层,但是最佳实践是一般domain不超过两层,也可以默认约定.io域名后缀全部是为api部署服务)。
- mobile表示面向移动端。
- v1表示接口版本。
- comment表示此类服务为评论服务。
4. 环境划分原则(Env)
整体服务架构的划分原则不在此文档讨论范围之内,而一个最佳实践是在domain当中就对环境作区分。
规范:https://api-[env-name].groupname.domain.io/
5. 接口的版本(API Version)
应该将API的版本号放入URL合适的层次。注意这里的Version既不表示客户端的版本,也不表示服务器中服务对应的版本,而是特指该接口的版本。一般用来处理对接口进行升级的情况。
规范:https://api-[env-name].groupname.domain.io/mobile/[version]/
- v1和v2的区别,应该表示且仅表示接口的区别。
- 不要发布无版本号的接口。
- 使用简单的数字。
- 服务分组加版本两个变量来共同决定接口的实现逻辑。
6. 路径(/端口,EndPoints)命名
提供以下原则供参考:路径表示API的具体URL,每一个URL唯一的表示一种资源,所以网址中不应该有动词,只应该有名词,而且所用的名词往往与代表的对象名称对应,一般来说是某一种记录的集合,所以API名词当中应该使用复数。
- URL
/cards/getCardById/{id}
=> HTTP GET: /cards/{id} - 如果某些动作是HTTP动词表达不了的,那么应该将动作当成资源去处理。
POST /accounts/1/transfer/500/to/2
=> POST /transaction?from=1&to=2&amount=500.00 - 为了保持简单,只对所有资源使用复数。
/setting => /settings
/user => /users - 资源之间的层级关系应该表述清楚。
GET /hotels/1312/homes/ 返回酒店1312的所有房间。
GET /hotels/1312/homes/1209 返回酒店1312的1209房间。
7. HTTP动词(HTTP Verbs)
以下括号中对应SQL的动词:
- GET(SELECT):从服务器取出对应的资源。
- POST(CREATE):新建一个资源。
- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整的资源,所以应该少用)。
- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
- DELETE(DELETE):删除资源。
最佳实践
- 此处的最佳实践是约定好动作处理原则,并且在整个系统(组织或者公司)内保持统一。
- GET方法不应该涉及状态改变。
- 很多时候我们还需要收集客户端的信息,但是REST本身是无状态的,所以收集的时候应该独立于REST API的设计原则,独立于设计体系来收集类似于client、cookie、ip和device等信息。
8. 不符合CRUD的情况
一般有如下建议:
- 使用POST重构行为的action。
- 增加控制参数(整体进行约定)。
- 将动作转换成资源。
9. 过滤信息(Filtering)
API应该提供参数,过滤返回结果。因为服务器端某个资源数量可能很多,比如用户的订单数、全国的酒店数等。过滤的语义应该包括对数据集合的过滤、排序、选择和分页等功能。
- ?limit=10
返回指定条目的数据 - ?offset=10
指定返回记录的开始位置 - ?pageNumber=2&perSize=100
指定第几页以及每页的记录数量 - ?sortBy=time&order=desc
指定排序属性及顺序 - ?type=1
指定筛选条件
最佳实践
- 实际上,我们在处理API业务时,不可能像数据库查询那么简洁容易,所以参数定义应该更加单一职责、更加严谨。
- 总是可以在输入参数中设计一个客户端需要的attrList,由使用者来指定需要的属性列表。
- 参数的设计上应该允许冗余。
- 可以使用HTTP的定制头:X-Total-Count表示资源总数。
10. 状态码(Status Codes)
- 这里一般实践是包含两层,一层是中间件(比如网关、nginx、tomcat)返回的HTTP请求本身的状态码,另外还包含服务本身返回的状态码设计。
- 一个好的可以坚持的原则是,服务本身返回的状态码的前缀应该和网关那一层保持一致,这样可以有最好的层次关系。
比如40001表示请求参数错误,其中400表示INVALID REQUEST,01表示具体的错误为参数错误。
11. 错误处理(Error Handling)
如果状态码不是正确的返回,就应该返回出错信息,尽量使用详细的错误信息,一个好的实践是出错信息应该包含:
- userMessage:显示给用户的
- internalMessage:显示给程序员调试用的
- code:编码
- guideline:参考解决指南
12. 返回结果(Response)
- 按照RESTful架构“宽进严出”的原则,返回应该被严格定义。
- 全部使用JSON返回结构。
- 基本约定:
GET /collection:返回资源列表
GET /collection/resource:返回单个对象
POST /collection/resource:返回新生成的对象
PUT /collection/resource:返回完整的更新后的资源对象
PATCH /collection/resource:返回完整的更新后的资源对象
DELETE /collection/resource:返回一个空文档
- 实际执行过程远比这个复杂,但是我们仍然可以有一些基本原则。
13. 使用HATEOAS构建Hypermedia APIs
HATEOAS(Hypermedia as the Engine of Application State),也即超媒体作为应用状态的引擎。超媒体API很可能是RESTful API设计的未来,它们实际上是一个非常惊人的概念,可以追溯到HTTP和HTML的工作原理。我们可以使用HATEOAS in Spring来构建Hypermedia APIs,而在此之前约定更加重要。
一个好的Hypermedia范例是:https://api.github.com/
{
"current_user_url": "https://api.github.com/user",
"current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}",
"authorizations_url": "https://api.github.com/authorizations",
"code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}",
"commit_search_url": "https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}",
"emails_url": "https://api.github.com/user/emails",
"emojis_url": "https://api.github.com/emojis",
"events_url": "https://api.github.com/events",
"feeds_url": "https://api.github.com/feeds",
"followers_url": "https://api.github.com/user/followers",
"following_url": "https://api.github.com/user/following{/target}",
"gists_url": "https://api.github.com/gists{/gist_id}",
"hub_url": "https://api.github.com/hub",
"issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}",
"issues_url": "https://api.github.com/issues",
"keys_url": "https://api.github.com/user/keys",
"label_search_url": "https://api.github.com/search/labels?q={query}&repository_id={repository_id}{&page,per_page}",
"notifications_url": "https://api.github.com/notifications",
"organization_url": "https://api.github.com/orgs/{org}",
"organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}",
"organization_teams_url": "https://api.github.com/orgs/{org}/teams",
"public_gists_url": "https://api.github.com/gists/public",
"rate_limit_url": "https://api.github.com/rate_limit",
"repository_url": "https://api.github.com/repos/{owner}/{repo}",
"repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}",
"current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}",
"starred_url": "https://api.github.com/user/starred{/owner}{/repo}",
"starred_gists_url": "https://api.github.com/gists/starred",
"user_url": "https://api.github.com/users/{user}",
"user_organizations_url": "https://api.github.com/user/orgs",
"user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}",
"user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}"
}
14. 标准请求定义
标准的请求定义当中有很多最佳实践,比如Content-Type等。好的实践是我们尽早用较小的代价将Content-Type、language等加入设计当中,可以避免后续很多问题。
15. 认证(Authentication)
认证的时候取决于API的使用者和生产者之间的关系以及需要保护的程度,目前此处的最佳实践是采用OAuth 2.0当中合适的模式来构建。
16. 文档(Documentation)
使用 Swagger API+JSON 进行文档管理和信息描述,定义一个标准的、语言无关的、供人和计算机理解服务的文档,类似于SOAP当中的WSDL。
- 需要满足API自动生成同步的在线文档。
- 可以用于API设计review。
- 方便测试人员了解API定义。
- 可以作为客户产品文档的一部分进行发布。
- 可以通过API Swagger文档生成使用者和生产者的骨架代码。