API 设计和文档规范

26 minute read

什么是REST

REST是一种基于超媒体构建分布式系统的架构风格,可以独立于任何基础协议。但是最常见的 REST API 实现都是基于 HTTP 作为应用程序协议,此文档仅针对基于 HTTP 设计的 REST API 。

基础知识(术语)

  • URI(Uniform Resource Identifiers) 统一资源标示符
  • URL(Uniform Resource Locator) 统一资源定位符

URI的格式定义如下:

URI = scheme "://" authority "/" path [ "?" query ] [ "#" fragment ]

URL是URI的一个子集(一种具体实现),对于REST API来说一个资源一般对应一个唯一的URI(URL)。在URI的设计中,遵循一些规则,使接口看起透明易读,方便使用者调用。

关于分隔符“/”的使用

“/“分隔符一般用来对资源层级的划分,对于REST API来说,"/“只是一个分隔符,并无其他含义。为了避免混淆,"/“不应该出现在URL的末尾。例如以下两个地址实际表示的都是同一个资源:

https://dningbo.com/api/v1/users/1
https://dningbo.com/api/v1/users/1/

REST API对URI资源的定义具有唯一性,一个资源对应一个唯一的地址。为了使接口保持清晰干净,如果访问到末尾包含 “/” 的地址,服务端应该301到没有 “/“的地址上。这个规则仅限于REST API接口的访问,并不适用于传统 Web 页面。

URI中使用连字符”-“代替下划线”_“的使用

连字符”-“一般用来分割URI中出现的字符串(单词),来提高URI的可读性,例如:

http://api.example.restapi.org/blogs/mark-masse/entries/this-is-my-first-post

使用下划线__来分割字符串(单词)可能会和链接的样式冲突重叠,而影响阅读性。实际上,"-“和"“对URL中字符串的分割语意上还是有些差异的:"-“分割的字符串(单词)一般各自都具有独立的含义,可参见上面的例子。而”_“一般用于对一个整体含义的字符串做了层级的分割,方便阅读,例如你想在URL中体现一个ip地址的信息:192_168_22_21 .

URI中统一使用小写字母

根据RFC3986定义,URI是对大小写敏感的,所以为了避免歧义,约定使用小写字符。但主机名(Host)和scheme(协议 名称:http/ftp/…)对大小写是不敏感的。

URI中不要包含文件(脚本)的扩展名

例如 .php .json 之内的就不要出现了,对于接口来说没有任何实际的意义。如果是想对返回的数据内容格式标示,通过HTTP Header中的Content-Type字段来配置。

资源类型

文档(Document)

文档是资源的单一表现形式,可以理解为一个对象,或者数据库中的一条记录。在请求文档时,要么返回文档对应的数据,要么会返回一个指向另外一个资源(文档)的链接。以下是几个基于文档定义的URI例子:

http://api.soccer.restapi.org/leagues/seattle 
http://api.soccer.restapi.org/leagues/seattle/teams/trebuchet 
http://api.soccer.restapi.org/leagues/seattle/teams/trebuchet/players/mike

集合(Collection)

集合可以理解为是资源的一个容器(目录),可以向里面添加资源(文档)。例如:

http://api.soccer.restapi.org/leagues 
http://api.soccer.restapi.org/leagues/seattle/teams 
http://api.soccer.restapi.org/leagues/seattle/teams/trebuchet/players

控制器(Controller)

控制器资源模型,可以执行一个方法,支持参数输入,结果返回。 是为了除了标准操作:增删改查(CRUD)以外的一些逻辑操作。控制器(方法)一般定义子URI中末尾,并且不会有子资源(控制器)。例如向用户重发 ID 为 245743的消息:

POST /alerts/245743/resend

URI 命名规范

  • 文档(Document)类型的资源用名词(短语)单数命名
  • 集合(Collection)类型的资源用名词(短语)复数命名
  • 控制器(Controller)类型的资源用 动词(短语) 命名
  • URI中有些字段可以是变量,在实际使用中可以按需替换

例如一个资源URI可以这样定义:

http://api.soccer.restapi.org/leagues/{leagueId}/teams/{teamId}/players/{playerId}

其中:leagueId,teamId,playerId 是变量(数字,字符串都类型都可以)。

  • CRUD的操作不要体现在URI中,HTTP协议中的操作符已经对CRUD做了映射

CRUD是创建,读取,更新,删除这四个经典操作的简称,例如删除的操作用REST规范执行的话,应该是这个样子:

DELETE /users/1234

以下是几个错误示例:

GET /deleteUser?id=1234
GET /deleteUser/1234
DELETE /deleteUser/1234
POST /users/1234/delete

HTTP响应状态码的使用

  • 200 (“OK”) 用于一般性的成功返回,不可用于请求错误返回
  • 201 (“Created”) 资源被创建
  • 202 (“Accepted”) 用于Controller控制类资源异步处理的返回,仅表示请求已经收到。对于耗时比较久的处理,一般用异步处理来完成
  • 204 (“No Content”) 此状态可能会出现在PUT、POST、DELETE的请求中,一般表示资源存在,但消息体中不会返回任何资源相关的状态或信息。
  • 301 (“Moved Permanently”) 资源的URI被转移,需要使用新的URI访问
  • 302 (“Found”) 不推荐使用,此代码在HTTP1.1协议中被303/307替代。我们目前对302的使用和最初HTTP1.0定义的语意是有出入的,应该只有在GET/HEAD方法下,客户端才能根据Location执行自动跳转,而我们目前的客户端基本上是不会判断原请求方法的,无条件的执行临时重定向
  • 303 (“See Other”) 返回一个资源地址URI的引用,但不强制要求客户端获取该地址的状态(访问该地址)
  • 304 (“Not Modified”) 有一些类似于204状态,服务器端的资源与客户端最近访问的资源版本一致,并无修改,不返回资源消息体。可以用来降低服务端的压力
  • 307 (“Temporary Redirect”) 目前URI不能提供当前请求的服务,临时性重定向到另外一个URI。在HTTP1.1中307是用来替代早期HTTP1.0中使用不当的302
  • 400 (“Bad Request”) 用于客户端一般性错误返回, 在其它4xx错误以外的错误,也可以使用400,具体错误信息可以放在body中
  • 401 (“Unauthorized”) 在访问一个需要验证的资源时,验证错误
  • 403 (“Forbidden”) 一般用于非验证性资源访问被禁止,例如对于某些客户端只开放部分API的访问权限,而另外一些API可能无法访问时,可以给予403状态
  • 404 (“Not Found”) 找不到URI对应的资源
  • 405 (“Method Not Allowed”) HTTP的方法不支持,例如某些只读资源,可能不支持POST/DELETE。但405的响应header中必须声明该URI所支持的方法
  • 406 (“Not Acceptable”) 客户端所请求的资源数据格式类型不被支持,例如客户端请求数据格式为application/xml,但服务器端只支持application/json
  • 409 (“Conflict”) 资源状态冲突,例如客户端尝试删除一个非空的Store资源
  • 412 (“Precondition Failed”) 用于有条件的操作不被满足时
  • 415 (“Unsupported Media Type”) 客户所支持的数据类型,服务端无法满足
  • 500 (“Internal Server Error”) 服务器端的接口错误,此错误于客户端无关

API设计原则

  • REST API 围绕资源进行设计,资源是指可由客户端访问的任何类型的对象、数据和服务。
  • 每个资源都有一个标识符,即拥有一个唯一标识该资源的 URI,比如:某一用户信息的 URL 可能是:
https://dningbo.com/users/1
  • 客户端通过交换资源的标识形式来与服务交互,公司统一使用 JSON 作为交换格式。如,对上文的 URI 进行 GET 请求可能返回以下响应:
{
    "name": "张三",
    "sex": 1,
    "age": 25
}
  • REST API 使用统一接口, 有助于实现客户端与服务分离。统一接口使用标准 HTTP 谓词对资源进行操作,常见的操作有:GET、 POST 、 PUT 、 PATCH 、 DELETE
  • REST API 使用无状态请求模型,请求是独立的并且可以按任意顺序发生。信息吃的唯一存储位置在资源内, 且每个请求都应该是原子操作。

围绕资源组织 API 设计

API 的操作针对的是一个个的业务实体,资源的 URI 应该基于名词(资源)而不是动词(对资源进行的操作)。比如:

https://dningbo.com/users             // 正确
https://dningbo.com/create-user      // 应避免

资源无需基于单个物理数据项目,比如,用户可以在内部实现为关系数据库中的多个表,单以单个实体的形式提供给客户端,应避免创建反映数据库内部结构的 API 。 REST 目标是为实体建模,以及为对这些实体执行的操作建模, 不应该将内部的实现公开给客户端。 实体通常分组成集合(用户、权限等)。集合是不同于集合中子项的资源,有自身的 URI,如以下 URI 表示用户的集合:

https://dningbo.com/users

向集合 URI 发送 GET 请求可以获取集合中的子项列表(如包含多个用户的列表)。集合中的每个子项也有自身唯一的 URI,对子项的 URI 发出 GET 请求会返回该子项的详细信息,比如想要获取 ID 为 1 的用户详细信息,请求的 URI 为:

https://dningbo.com/users/1

在 URI 中采用一致的命名约定。如上文所述, 对引用集合的 URI 使用复数名词,集合和集合内的子项组成层次结构。例如:/users 表示用户集合,/users/1表示 ID 为 1 的用户路径。这有助于使 API 保持直观, 并且方便基于参数化 URI 路径来定义路由,如此处可以定义/users/{id}为统一的用户详情路由。

不用类型的资源之间存在关系,如 /users/1/orders可以表示用户 1 的所有订单, 同理,/orders/8/users可以表示订单 8 中的所有用户。虽然这样的方式非常灵活,但是过度扩展这个模型会变得难以实现和维护。我们约定, 仅支持 2 级的关系检索。

根据 HTTP 方法定义 API 操作

HTTP 协议定义了大量为请求赋予语义的方法,大多数 REST API 常见的HTTP方法如下:

  • GET 检索位于指定 URI 处的资源,响应消息的正文包含所请求资源的详细信息。
  • POST 在指定的 URI 处创建新资源,请求消息的正文提供新资源的详细信息。也用于其他会对资源造成变更的操作。
  • PUT 在指定 URI 处更新资源(即修改),请求消息的正文要指定要更新的资源和更新的内容。
  • PATCH 对指定资源进行部分更新,请求正文要包含更改的内容。
  • DELETE 删除指定 URI 上的资源。 示例:
资源 POST GET PUT PATCH DELETE
/users 创建新用户 检索所有用户 批量更新用户 批量部分更新用户 删除所有用户
/users/1 - 检索客户 1 的详细信息 客户 1 不存在就创建,如果客户 1 存在,则更新其详细信息 部分更新用户 1 信息 删除用户 1
/users/1/orders 创建用户 1 的新订单 检索用户 1 的所有订单 批量更新用户 1 的订单 部分更新用户 1 的订单 删除用户 1 的所有订单

POST、 PUT 、 PATCH 之间因为功能存在重叠,容易引起混淆,我们约定如下:

  • POST 请求主要用于创建资源和其他不适合其他请求方式的操作。
  • PUT 请求用于具体的资源而不是集合,创建或者更新现有的资源。
  • PATCH 请求用于对现有资源进行部分更新,比 PUT 更加高效,仅需要发送需要更新的数据,不需要发送所有详细信息。

注意 PUT 操作是幂等的, 即多次提交同一个 PUT 请求,结果始终相同,而 POST 和 PATCH 的幂等性无法保证。

GET 方法

请求成功返回 HTTP 状态码 200(正常), 找不到资源返回 404(未找到)

POST 方法

如果用于创建资源, 请求成功返回HTTP 状态码 201(已创建),非创建资源的操作成功时返回 200(成功),请求数据错误时返回 400(错误的请求)。

PUT 方法

与POST类型,创建了新资源时返回 201(已创建),更新了资源时返回 200(成功)。

PATCH 方法

请求成功返回 200(成功),请求参数有误时返回 400(错误的亲求)。

DELETE 方法

请求成功时应返回 204(无内容),表示已经正确删除该资源,响应正文中没有信息。如果操作的资源不存在返回 404(未找到)。

响应消息为空

任何请求,如果响应的消息正文为空, HTTP 状态码都应该为 204(无内容)。

数据筛选和分页

当使用 GET 请求检索数据,返回数据量较大时候需要支持筛选和分页。 允许在 URI 的查询字符串中传递筛选器,比如/users?name=张三 ,服务端负责分析和处理字符串中的 name 并返回筛选后的结果。 同时支持在查询字符串中指定要检索的页码和每次检索的数据数量,例如:

/users?page=1&size=50

API 版本控制

API 一直保持静态不变更的可能性很小,随着业务需求变化,会产生新的资源集合, 资源之间的关系会发生变更,已有资源的数据和操作也会发生变更。一般情况下,在新 API 发布时,需要保证一段时间内旧 API 可以正常运行。 统一约定在 URI 中添加版本号来支持 API 版本,例如:

https://dningbo.com/api/v1/users

验证和授权

任何人随意访问公开的 API 是不好的做法,验证和授权是两件事情:

  • 验证(Authentication)是为了确定用户是其申明的身份,比如提供账户的密码。不然的话,任何人伪造成其他身份(比如其他用户或者管理员)是非常危险的
  • 授权(Authorization)是为了保证用户有对请求资源特定操作的权限。比如用户的私人信息只能自己能访问,其他人无法看到;有些特殊的操作只能管理员可以操作,其他用户有只读的权限等等

如果没有通过验证(提供的用户名和密码不匹配,token 不正确等),需要返回 401 Unauthorized状态码,并在 body 中说明具体的错误信息;而没有被授权访问的资源操作,需要返回 403 Forbidden 状态码,还有详细的错误信息。

优秀API文档必备要素

  • 易学习 有完善的文档及提供尽可能多的示例,文档要遵循行业和国际标准,让有经验和背景的调用者可以快速上手。 例如: 定义国家代码,要使用ISO-3316国家代码标准中的国家代码,中国是“CN”,而不是另造一个,以免造成误解。
  • 易使用
    没有复杂的程序和业务细节,易于读者学习。灵活的 API 允许按字段排序、自定义分页、排序和筛选。
  • 难误用 有详细的错误听故事,用户可从文档中了解 API 的使用方法和误区,不会在调用 API 的使用出现文档中找不到情况描述的情况。
  • 兼容性 API 的升级要考虑乡下兼容,文档也要考虑前后一致。例如:尽量避免在版本 1 中是必填的参数,在版本 2 中改为非必填,等等。
  • 实时性 文档要及时根据 API 的更新而更新,与服务一致,避免出现用户根据老的文档请求新接口而造成的与预期一致,因此,使用在线文档是更好的方式。

标准文档格式

1.修订历史
2.接口描述
3.URI地址
4.请求方法
5.请求和返回参数
    请求参数
    返回参数
6.成功和异常示例
    成功示例
        请求参数
        返回参数
    异常示例
        请求参数
        返回参数
7.状态码
    错误码
    业务码

文档示例

修订历史

  • 2023-08-1 新增接口
  • 2023-08-15 添加xx字段

接口描述

获取token,用于鉴权等等描述。

URI地址

/oauth2/token

请求方法

POST

请求和返回参数

请求参数
  • code String 获取到的code
  • grant_type String 授权类型
  • client_id String 客户端分配的ID
  • ……
返回参数
  • access_token String 返回的access_token
  • expires_in Integer 过期时间
  • ……

成功和异常示例

请求:

curl https://dningbo.com/oauth2/token \
    -d code=<AUTHORIZATION_CODE> \
    -d grant_type=authorization_code \
    -d redirect_uri=<REDIRECT_URI> \
    -d client_id=<APP_KEY> \
    -d client_secret=<APP_SECRET>

成功返回:

{
  "access_token": "sl.AbX9y6Fe3AuH5o66-gmJpR032jwAwQPIVVzWXZNkdzcYT02akC2de219dZi6gxYPVnYPrpvISRSf9lxKWJzYLjtMPH-d9fo_0gXex7X37VIvpty4-G8f4-WX45AcEPfRnJJDwzv-",
  "expires_in": 14400,
  "token_type": "bearer",
  "scope": "account_info.read files.content.read files.content.write files.metadata.read",
  "account_id": "dbid:AAH4f99T0taONIb-OurWxbNQ6ywGRopQngc",
  "uid": "12345"
}

异常返回:

http code: 400,用户错误
{
  "error_summary": "invalid_access_token/...",
  "error": {
    ".tag": "invalid_access_token"
  }
}

http code: 401,没有权限
{
  "error_summary": "no_team_api_access/...",
  "error": {
    ".tag": "no_team_api_access"
  }
}
......

状态码和业务码(示例)

状态码:与 HTTP Code 设计保持一致

状态码 含义
30x 跳转 xx
40x 客户端错误 xxx

业务码:自定义,包含业务含义,与状态码有区别

业务码 含义
1100 业务1成功
2200 业务流程失败