– __服務演變__。 API 應能在不影響客戶端應用程序的情況下改進和添加功能。 隨著 API 的發展,現有客戶端應用程序應可繼續運行而無需進行任何修改。 所有功能應該是可發現的,使客戶端應用程序能夠充分利用它。

__RESTful API成熟度模型:__

2008 年,Leonard Richardson 提議對 RESTful API 使用以下[成熟度模型](https://martinfowler.com/articles/richardsonMaturityModel.html):

– 級別 0:定義一個 URI,所有操作是對此 URI 發出的 [POST](http://www.dlbhg.com/provider/uid2024120814472139bfa9) 請求。
– 級別 1:為各個資源單獨創建 URI。
– 級別 2:使用 [HTTP](http://www.dlbhg.com/wiki/what-are-http-and-https/) 方法來定義對資源執行的操作。
– 級別 3:使用[超媒體](http://www.dlbhg.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](http://www.dlbhg.com/blog/rest_api_vs_open_api)關系不明,很容易把滿足OpenAPI規范的API 等同于 RESTful API 。
– [Web API 和 RESTful API](http://www.dlbhg.com/blog/web_api_vs_rest_api/)關系不明,在使用時會概念互換。
– 其它一些[REST API概念常見問題](http://www.dlbhg.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設計](http://www.dlbhg.com/wiki/api-design/)概要,本文重點講述API設計的基本方法、主體框架。
– [RESTful API狀態碼](http://www.dlbhg.com/wiki/restful-api-best-practices-02/)使用指南,重點講述如何用好[HTTP 狀態](http://www.dlbhg.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](http://www.dlbhg.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](http://www.dlbhg.com/wiki/xml/)曾經也火過一把(通常在一個大企業級環境下)。這要感謝[SOAP](http://www.dlbhg.com/wiki/soap-api/),不過它已經掛了,并且我們也沒看到太多的API把[HTML](http://www.dlbhg.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](http://www.dlbhg.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安全設計指導](http://www.dlbhg.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)

一站搜索、試用、比較全球API!
冪簡集成已收錄 4968種API!
試用API,一次比較多個渠道