RESTful API设计概要
RESTful API(也称为RESTful Web API)已经是在线开放API实施上的事实标准,企业API开放平台大多都会采用OpenAPI做为RESTful API的API描述规范,以此来支持市面上 大部分 API生态相关的工具。一个设计良好的开放API,不仅能够有效规避API安全漏洞,还能借助开放API平台实现API的货币化,成为企业营收的重要来源。
做出一个好的[RESTful API](https://www.explinks.com/provider/uid2024110274502138c7c9)设计很难。[API](https://www.explinks.com/wiki/api/)表达的是你的数据和你的数据使用者之间的契约。打破这个契约将会招致很多愤怒的邮件,和一大堆伤心的用户-因为他们手机上的App不工作了。而[API文档](https://www.explinks.com/wiki/api-docs/)化只能达到一半的效果,并且也很难找到一个愿意写文档的程序员。
你所能做的最重要一件事来提高服务的价值就是创建一个API。因为随着其他服务的成长,有这样一个API会使你的服务或者核心应用将有机会变成一个平台。环顾一下现有的这些大公司:[Facebook](https://www.explinks.com/provider/uid202410248040179a1d86),Twitter,[Google](https://www.explinks.com/provider/uid202406184232146efdc5), Github,Amazon,Netflix等。如果当时他们没有通过API来开放数据的话,也不可能成长到如今的规模。
__设计良好的 RESTful API 原则:__
– __平台独立性__。 不管 API 的内部实现方式如何,任何客户端都应该能够调用该 API。 这就需要使用标准协议并创建一种机制,使客户端和 Web 服务能够就交换数据的格式达成一致。
– __服务演变__。 API 应能在不影响客户端应用程序的情况下改进和添加功能。 随着 API 的发展,现有客户端应用程序应可继续运行而无需进行任何修改。 所有功能应该是可发现的,使客户端应用程序能够充分利用它。
__RESTful API成熟度模型:__
2008 年,Leonard Richardson 提议对 RESTful API 使用以下[成熟度模型](https://martinfowler.com/articles/richardsonMaturityModel.html):
– 级别 0:定义一个 URI,所有操作是对此 URI 发出的 [POST](https://www.explinks.com/provider/uid2024120814472139bfa9) 请求。
– 级别 1:为各个资源单独创建 URI。
– 级别 2:使用 [HTTP](https://www.explinks.com/wiki/what-are-http-and-https/) 方法来定义对资源执行的操作。
– 级别 3:使用[超媒体](https://www.explinks.com/wiki/hypermedia/)(HATEOAS,如下所述)。
## 设计__RESTful API__ 的一些基本约束
– __RESTful API 不应依赖于任何单一通信协议__,尽管其成功映射到给定协议可能取决于元数据的可用性、方法的选择等。通常,任何使用 URI 进行标识的协议元素都必须允许使用任何 URI 方案来进行标识。*[此处的失败意味着标识与交互没有分离。]*
– 除了填写或修复标准协议中未指定部分的细节(例如 HTTP 的 PATCH 方法或 Link 标头字段)之外,__RESTful API 不应包含对通信协议的任何更改__。针对损坏的实现(例如那些愚蠢到相信 HTML 定义 HTTP 方法集的浏览器)的解决方法应该单独定义,或至少在附录中定义,并期望解决方法最终会被淘汰。*[此处的失败意味着资源接口是特定于对象的,而不是通用的。]*
– __RESTful API 应将其几乎所有的描述性工作都花在定义用于表示资源和驱动应用程序状态的媒体类型上__,或者花在为现有标准媒体类型定义扩展关系名称和/或支持超文本的标记上。描述对感兴趣的 URI 使用什么方法的任何工作都应完全在媒体类型的处理规则范围内定义(在大多数情况下,现有媒体类型已经定义了)。*[此处的失败意味着带外信息正在驱动交互而不是超文本。]*
– __RESTful API 不得定义固定的资源名称或层次结构__(客户端和服务器的明显耦合)。服务器必须有控制自己命名空间的自由。相反,允许服务器指导客户端如何构造适当的 URI,例如在 HTML 表单和 URI 模板中所做的那样,通过在媒体类型和链接关系中定义这些指令。*[此处的失败意味着客户端由于带外信息(例如特定于域的标准,这是面向数据的 RPC 功能耦合的等价物)而假设资源结构]。*
– __RESTful API 永远不应该具有对客户端来说很重要的“类型化”资源__。规范作者可以使用资源类型来描述接口背后的服务器实现,但这些类型必须与客户端无关且不可见。对客户端来说唯一重要的类型是当前表示的媒体类型和标准化关系名称。*[同上]*
– 除了初始 URI(书签)和适合目标受众的标准化媒体类型集(即,预期任何可能使用该 API 的客户端都能理解)之外,__RESTful API 不应具有任何先验知识__。从那时起,所有应用程序状态转换都必须由客户端选择服务器提供的选项来驱动,这些选项存在于接收的表示中或由用户对这些表示的操作暗示。转换可能由客户端对媒体类型和资源通信机制的了解决定(或限制),这两者都可以即时改进(例如,按需编码)。 *[此处的失败意味着带外信息正在驱动交互而不是超文本。]*
### RESTful API设计时容易混淆的概念问题
– RPC 和 RESTful API 区分不明,很容易把基于HTTP的RPC 风格认定为RESTful 风格。
– [OpenAPI 和 RESTful API](https://www.explinks.com/blog/rest_api_vs_open_api)关系不明,很容易把满足OpenAPI规范的API 等同于 RESTful API 。
– [Web API 和 RESTful API](https://www.explinks.com/blog/web_api_vs_rest_api/)关系不明,在使用时会概念互换。
– 其它一些[REST API概念常见问题](https://www.explinks.com/blog/api-questions/)清单。
### RESTful API设计用到的一些术语
– REST资源__/__Resource__:__一个对象的单独实例,如一只动物
– REST Payload:指承载REST报文的格式
– 集合/Collection__:__一群同种对象,如动物
– HTTP__:__跨网络的通信协议
– 客户端/Consumer__:__可以创建HTTP请求的客户端应用程序
– 第三方开发者/Third Party Developer__:__这个开发者不属于你的项目但是有想使用你的数据
– 服务器/Server__:__一个HTTP服务器或者应用程序,客户端可以跨网络访问它
– 端点/Endpoint__:__这个API在服务器上的URL用于表达一个资源或者一个集合
– 幂等/Idempotent__:__无边际效应,多次操作得到相同的结果
– URL段/URL Segment__:__在URL里面已斜杠分隔的内容
## RESTful API设计指南
– RESTful [API设计](https://www.explinks.com/wiki/api-design/)概要,本文重点讲述API设计的基本方法、主体框架。
– [RESTful API状态码](https://www.explinks.com/wiki/restful-api-best-practices-02/)使用指南,重点讲述如何用好[HTTP 状态](https://www.explinks.com/wiki/rest_api_http_codes/)码,而不是重复造车。
## RESTful API设计主体框架
### Protocol
客户端在通过 `API` 与后端服务通信的过程中,`应该` 使用 `HTTPS` 协议。
### Root URL
`API` 的根入口点应尽可能保持足够简单,这里有两个常见的 `URL` 根例子:
– api.example.com/*
– example.com/api/*
> 如果你的应用很庞大或者你预计它将会变的很庞大,那 `应该` 将 `API` 放到子域下(`api.example.com`)。这种做法可以保持某些规模化上的灵活性。
### Versioning
所有的 `API` 必须保持向后兼容,你 `必须` 在引入新版本 `API` 的同时确保旧版本 `API` 仍然可用。所以 `应该` 为其提供版本支持。
目前比较常见的两种版本号形式,至于具体把版本号放在什么地方,这个问题一直存在很大的争议:
– 在 URL 中嵌入版本编号,__api.example.com/v1/*__,这种做法是版本号直观、易于调试;
– 将版本号放在 `HTTP Header` 头中,Accept: application/vnd.example.com.v1+json
## 一、Endpoints/URL 设计
Endpoints(端点)设计,也就是URL设计,或称为命名规则:
– URL 的命名 `必须` 全部小写
– URL 中资源(`resource`)的命名 `必须` 是名词,并且 `必须` 是复数形式
– `必须` 优先使用 `Restful` 类型的 URL
– URL `必须` 是易读的
– URL `一定不可` 暴露服务器架构
至于 URL 是否必须使用连字符(`-`) 或下划线(`_`),不做硬性规定,但 `必须` 根据团队情况统一一种风格。
### 1.1 动词 + 宾语
RESTful 的核心思想就是,客户端发出的数据操作指令都是”动词 + 宾语”的结构。比如,`GET /articles`这个命令,`GET`是动词,`/articles`是宾语。
动词通常就是五种 HTTP 方法,对应 [CRUD](https://www.explinks.com/wiki/what-is-crud/) 操作。
> – GET(SELECT):从服务器取出资源(一项或多项)。
> – POST(CREATE):在服务器新建一个资源。
> – PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
> – PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
> – DELETE(DELETE):从服务器删除资源。
根据 HTTP 规范,动词一律大写。
针对每一个端点(URL)来说,下面列出所有可行的 `HTTP` 动词和端点的组合
| 请求方法 | URL | 描述 |
| — | — | — |
| GET | /zoos | 列出所有的动物园(ID和名称,不要太详细) |
| POST | /zoos | 新增一个新的动物园 |
| GET | /zoos/{zoo} | 获取指定动物园详情 |
| PUT | /zoos/{zoo} | 更新指定动物园(整个对象) |
| PATCH | /zoos/{zoo} | 更新动物园(部分对象) |
| DELETE | /zoos/{zoo} | 删除指定动物园 |
| GET | /animal_types | 获取所有动物类型(ID和名称,不要太详细) |
| GET | /animal_types/{type} | 获取指定的动物类型详情 |
| GET | /employees | 检索整个雇员列表 |
| GET | /employees/{employee} | 检索指定特定的员工 |
| GET | /zoos/{zoo}/employees | 检索在这个动物园工作的雇员的名单(身份证和姓名) |
| POST | /employees | 新增指定新员工 |
| POST | /zoos/{zoo}/employees | 在特定的动物园雇佣一名员工 |
| DELETE | /zoos/{zoo}/employees/{employee} | 从某个动物园解雇一名员工 |
### 1.2 动词的覆盖
有些客户端只能使用`GET`和`POST`这两种方法。服务器必须接受`POST`模拟其他三个方法(`PUT`、`PATCH`、`DELETE`)。
这时,客户端发出的 HTTP 请求,要加上`X-HTTP-Method-Override`属性,告诉服务器应该使用哪一个动词,覆盖`POST`方法。
> “`
> “`
>
> POST /api/Person/4 HTTP/1.1
> X-HTTP-Method-Override: PUT
>
> “`
> “`
上面代码中,`X-HTTP-Method-Override`指定本次请求的方法是`PUT`,而不是`POST`。
### 1.3 宾语必须是名词
宾语就是 API 的 URL,是 HTTP 动词作用的对象。它应该是名词,不能是动词。比如,`/articles`这个 URL 就是正确的,而下面的 URL 不是名词,所以都是错误的。
> – /getAllCars
> – /createNewCar
> – /deleteAllRedCars
### 1.4 复数 URL
既然 URL 是名词,那么应该使用复数,还是单数?
这没有统一的规定,但是常见的操作是读取一个集合,比如`GET /articles`(读取所有文章),这里明显应该是复数。
为了统一起见,建议都使用复数 URL,比如`GET /articles/2`要好于`GET /article/2`。
### 1.5 避免多级 URL
常见的情况是,资源需要多级分类,因此很容易写出多级的 URL,比如获取某个作者的某一类文章。
> “`
> “`
>
> GET /authors/12/categories/2
>
> “`
> “`
这种 URL 不利于扩展,语义也不明确,往往要想一会,才能明白含义。
更好的做法是,除了第一级,其他级别都用查询字符串表达。
> “`
> “`
>
> GET /authors/12?categories=2
>
> “`
> “`
下面是另一个例子,查询已发布的文章。你可能会设计成下面的 URL。
> “`
> “`
>
> GET /articles/published
>
> “`
> “`
查询字符串的写法明显更好。
> “`
> “`
>
> GET /articles?published=true
>
> “`
> “`
## 二、状态码
### 2.1 状态码必须精确
客户端的每一次请求,服务器都必须给出回应。回应包括 HTTP 状态码和数据两部分。
HTTP 状态码就是一个三位数,分成五个类别。
> – `1xx`:相关信息
> – `2xx`:操作成功
> – `3xx`:重定向
> – `4xx`:客户端错误
> – `5xx`:服务器错误
这五大类总共包含[100多种](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes)状态码,覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。
API 不需要`1xx`状态码,下面介绍其他四类状态码的精确含义。
### 2.2 2xx 状态码
`200`状态码表示操作成功,但是不同的方法可以返回更精确的状态码。
> – GET: 200 OK
> – POST: 201 Created
> – PUT: 200 OK
> – PATCH: 200 OK
> – DELETE: 204 No Content
上面代码中,`POST`返回`201`状态码,表示生成了新的资源;`DELETE`返回`204`状态码,表示资源已经不存在。
此外,`202 Accepted`状态码表示服务器已经收到请求,但还未进行处理,会在未来再处理,通常用于异步操作。下面是一个例子。
> “`
> “`
>
> HTTP/1.1 202 Accepted
>
> {
> “task”: {
> “href”: “/api/company/job-management/jobs/2130040”,
> “id”: “2130040”
> }
> }
>
> “`
> “`
### 2.3 3xx 状态码
API 用不到`301`状态码(永久重定向)和`302`状态码(暂时重定向,`307`也是这个含义),因为它们可以由应用级别返回,浏览器会直接跳转,API 级别可以不考虑这两种情况。
API 用到的`3xx`状态码,主要是`303 See Other`,表示参考另一个 URL。它与`302`和`307`的含义一样,也是”暂时重定向”,区别在于`302`和`307`用于`GET`请求,而`303`用于`POST`、`PUT`和`DELETE`请求。收到`303`以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。下面是一个例子。
> “`
> “`
>
> HTTP/1.1 303 See Other
> Location: /api/orders/12345
>
> “`
> “`
### 2.4 4xx 状态码
`4xx`状态码表示客户端错误,主要有下面几种。
`400 Bad Request`:服务器不理解客户端的请求,未做任何处理。
`401 Unauthorized`:用户未提供身份验证凭据,或者没有通过身份验证。
`403 Forbidden`:用户通过了身份验证,但是不具有访问资源所需的权限。
`404 Not Found`:所请求的资源不存在,或不可用。
`405 Method Not Allowed`:用户已经通过身份验证,但是所用的 HTTP 方法不在他的权限之内。
`410 Gone`:所请求的资源已从这个地址转移,不再可用。
`415 Unsupported Media Type`:客户端要求的返回格式不支持。比如,API 只能返回 JSON 格式,但是客户端要求返回 XML 格式。
`422 Unprocessable Entity` :客户端上传的附件无法处理,导致请求失败。
`429 Too Many Requests`:客户端的请求次数超过限额。
### 2.5 5xx 状态码
`5xx`状态码表示服务端错误。一般来说,API 不会向用户透露服务器的详细信息,所以只要两个状态码就够了。
`500 Internal Server Error`:客户端请求有效,服务器处理时发生了意外。
`503 Service Unavailable`:服务器无法处理请求,一般用于网站维护状态。
## 三、服务器回应
内容类型(Content Type)是返回设计中最重要的一个部分。
目前,大多数“精彩”的API都为RESTful API提供JSON数据。诸如Facebook,Twitter,Github等等你所知的。[XML](https://www.explinks.com/wiki/xml/)曾经也火过一把(通常在一个大企业级环境下)。这要感谢[SOAP](https://www.explinks.com/wiki/soap-api/),不过它已经挂了,并且我们也没看到太多的API把[HTML](https://www.explinks.com/wiki/what-is-html/)作为结果返回给客户端(除非你在构建一个爬虫程序)。
只要你返回给他们有效的数据格式,开发者就可以使用流行的语言和框架进行解析。如果你正在构建一个通用的响应对象,通过使用一个不同的序列化器,你也可以很容易的提供之前所提到的那些数据格式(不包括SOAP)。而你所要做的就是把使用方式放在响应数据的接收头里面。
有些API的创建者会推荐把.json, .xml, .html等文件的扩展名放在URL里面来指示返回内容类型,但我个人并不习惯这么做。我依然喜欢通过接收头来指示返回内容类型(这也是HTTP标准的一部分),并且我觉得这么做也比较适当一些。
### 3.1 不要返回纯本文
API 返回的数据格式,不应该是纯文本,而应该是一个 JSON 对象,因为这样才能返回标准的结构化数据。所以,服务器回应的 HTTP 头的`Content-Type`属性要设为`application/json`。
客户端请求时,也要明确告诉服务器,可以接受 JSON 格式,即请求的 HTTP 头的`ACCEPT`属性也要设成`application/json`。下面是一个例子。
> “`
> “`
>
> GET /orders/2 HTTP/1.1
> Accept: application/json
>
> “`
> “`
### 3.2 发生错误时,不要返回 200 状态码
有一种不恰当的做法是,即使发生错误,也返回`200`状态码,把错误信息放在数据体里面,就像下面这样。
> “`
> “`
>
> HTTP/1.1 200 OK
> Content-Type: application/json
>
> {
> “status”: “failure”,
> “data”: {
> “error”: “Expected at least two items in list.”
> }
> }
>
> “`
> “`
上面代码中,解析数据体以后,才能得知操作失败。
这张做法实际上取消了状态码,这是完全不可取的。正确的做法是,状态码反映发生的错误,具体的错误信息放在数据体里面返回。下面是一个例子。
> “`
> “`
>
> HTTP/1.1 400 Bad Request
> Content-Type: application/json
>
> {
> “error”: “Invalid payoad.”,
> “detail”: {
> “surname”: “This field is required.”
> }
> }
>
> “`
> “`
### 3.3 提供链接
API 的使用者未必知道,URL 是怎么设计的。一个解决方法就是,在回应中,给出相关链接,便于下一步操作。这样的话,用户只要记住一个 URL,就可以发现其他的 URL。这种方法叫做 HATEOAS。
举例来说,GitHub 的 API 都在 api.github.[com](https://www.explinks.com/provider/uid20241115192306ea69b0) 这个域名。访问它,就可以得到其他 URL。
> “`
> “`
>
> {
> …
> “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“,
> …
> }
>
> “`
> “`
上面的回应中,挑一个 URL 访问,又可以得到别的 URL。对于用户来说,不需要记住 URL 设计,只要从 api.github.com 一步步查找就可以了。
HATEOAS 的格式没有统一规定,上面例子中,GitHub 将它们与其他属性放在一起。更好的做法应该是,将相关链接与其他属性分开。
> “`
> “`
>
> HTTP/1.1 200 OK
> Content-Type: application/json
>
> {
> “status”: “In progress”,
> “links”: {[
> { “rel”:”cancel”, “method”: “delete”, “href”:”/api/status/12345″ } ,
> { “rel”:”edit”, “method”: “put”, “href”:”/api/status/12345″ }
> ]}
> }
>
> “`
> “`
## 四、请求参数设计与抽象
规划好你的API的外观要先于开发它实际的功能。首先你要知道数据该如何设计和核心服务/应用程序会如何工作。如果你纯粹新开发一个API,这样会比较容易一些。但如果你是往已有的项目中增加API,你可能需要提供更多的抽象。
有时候一个集合可以表达一个数据库表,而一个资源可以表达成里面的一行记录,但是这并不是常态。事实上,你的API应该尽可能通过抽象来分离数据与业务逻辑。这点非常重要,只有这样做你才不会打击到那些拥有复杂业务的第三方开发者,否则他们是不会使用你的API的。
## 五、响应数据过滤(Filtering)设计
> 如果记录数量很多,服务器不可能都将它们返回给用户。API `应该` 提供参数,过滤返回结果。下面是一些常见的参数。
– ?limit=10:指定返回记录的数量
– ?offset=10:指定返回记录的开始位置。
– ?page=2&per_page=100:指定第几页,以及每页的记录数。
– ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
– ?animal_type_id=1:指定筛选条件
所有 `URL` 参数 `必须` 是全小写,`必须` 使用下划线类型的参数形式。
> 分页参数 `必须` 固定为 `page`、`per_page`
经常使用的、复杂的查询 `应该` 标签化,降低维护成本。如
“`
GET /trades?status=closed&sort=sortby=name&order=asc
# 可为其定制快捷方式
GET /trades/recently_closed
“`
## 六、API安全设计
应该使用 `OAuth2.0` 的方式为 API 调用者提供登录认证。`必须` 先通过登录接口获取 `Access Token` 后再通过该 `token` 调用需要身份认证的 `API`。
详细内容,请阅读 [新手API安全设计指导](https://www.explinks.com/blog/base-rest-api-design-safety-tutorial/(在新窗口中打开))。
## 七、API文档设计
即使你不能百分之百的遵循指南中的条款,你的API也不是那么糟糕。但是,如果你不为API准备文档的话,没有人会知道怎么使用它,那它真的会成为一个糟糕的API。
– 让你的文档对那些未经认证的开发者也可用
– 不要使用文档自动化生成器,即便你用了,你也要保证自己审阅过并让它具有更好的版式。
– 不要截断示例中请求与响应的内容,要展示完整的东西。并在文档中使用高亮语法。
– 文档化每一个端点所预期的响应代码和可能的错误消息,和在什么情况下会产生这些的错误消息
借助成熟的API文档生成工具,你能很方便的设计美观、易读、便于打印的API文档。
## 参考资料
[RESTful 设计规范](https://godruoyi.com/posts/the-resetful-api-design-specification)
[RESTful API 最佳实践](https://www.ruanyifeng.com/blog/2018/10/restful-api-best-practices.html)
RESTful API Design: 13 Best Practices to Make Your Users Happy, by Florimond Manca,原文链接中断,可访问[国内原文转载](https://www.cnblogs.com/mouseleo/p/10820987.html)。
[API design](https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design), by MicroSoft Azure
[restful-api-design-references](https://github.com/aisuhua/restful-api-design-references)
[Principles of good RESTful API Design(译)](http://www.cnblogs.com/moonz-wu/p/4211626.html)