RESETful API 設計規範

godruoyi發表於2018-06-25

file

本文是為 大渝網 API 開發規範擬定的一個 beta 版,文章大量參考了目前比較常見的 RESETful API 設計。

為了更好的討論規範帶來的爭議及問題,現已把該文件整理並開源到 github,關於大家補充及提 issue

關於「能願動詞」的使用

為了避免歧義,文件大量使用了「能願動詞」,對應的解釋如下:

  • 必須 (MUST):絕對,嚴格遵循,請照做,無條件遵守;
  • 一定不可 (MUST NOT):禁令,嚴令禁止;
  • 應該 (SHOULD) :強烈建議這樣做,但是不強求;
  • 不該 (SHOULD NOT):強烈不建議這樣做,但是不強求;
  • 可以 (MAY)可選 (OPTIONAL) :選擇性高一點,在這個文件內,此詞語使用較少;

參見:RFC 2119

協議

在通過 API 於後端服務通訊的過程中,應該 使用 HTTPS 協議。

API Root URL

API 的根入口點應儘可能保持足夠簡單,這裡有兩個常見的 URL 根例子:

  • api.example.com/*
  • example.com/api/*

如果你的應用很龐大或者你預計它將會變的很龐大,那 應該API 放到子域下。這種做法可以保持某些規模化上的靈活性。

Versioning

所有的 API 必須保持向後相容,你 必須 在引入新版本 API 的同時確保舊版本 API 仍然可用。所以 應該 為其提供版本支援。

目前比較常見的兩種版本號形式:

在 URL 中嵌入版本編號

api.example.com/v1/*
複製程式碼

這種做法是版本號直觀、易於除錯;另一種做法是,將版本號放在 HTTP Header 頭中:

通過媒體型別來指定版本資訊

Accept: application/vnd.example.com.v1+json
複製程式碼

其中 vnd 表示 Standards Tree 標準樹型別,有三個不同的樹: xprsvnd。你使用的標準樹需要取決於你開發的專案

  • 未註冊的樹(x)主要表示本地和私有環境
  • 私有樹(prs)主要表示沒有商業釋出的專案
  • 供應商樹(vnd)主要表示公開發布的專案

後面幾個引數依次為應用名稱(一般為應用域名)、版本號、期望的返回格式。

至於具體把版本號放在什麼地方,這個問題一直存在很大的爭議,但由於我們大多數時間都在使用 Laravel 開發,應該 使用 dingo/api 來快速構建應用,它採用第二種方式來管理 API 版本,並且已整合了標準的 HTTP Response

Endpoints

端點就是指向特定資源或資源集合的 URL。在端點的設計中,你 必須 遵守下列約定:

  • URL 的命名 必須 全部小寫
  • URL 中資源(resource)的命名 必須 是名詞,並且 必須 是複數形式
  • 必須 優先使用 Restful 型別的 URL
  • URL 中不能出現 -必須 用下劃線 _ 代替
  • URL 必須 是易讀的
  • URL 一定不可 暴露伺服器架構

來看一個反例

  • https://api.example.com/getUserInfo?userid=1
  • https://api.example.com/getusers
  • https://api.example.com/sv/u
  • https://api.example.com/cgi-bin/users/get_user.php?userid=1

再來看一個正列

  • https://api.example.com/zoos
  • https://api.example.com/animals
  • https://api.example.com/zoos/{zoo}/animals
  • https://api.example.com/animal_types
  • https://api.example.com/employees

HTTP 動詞

對於資源的具體操作型別,由 HTTP 動詞表示。常用的 HTTP 動詞有下面五個(括號裡是對應的 SQL 命令)。

  • GET(SELECT):從伺服器取出資源(一項或多項)。
  • POST(CREATE):在伺服器新建一個資源。
  • PUT(UPDATE):在伺服器更新資源(客戶端提供改變後的完整資源)。
  • PATCH(UPDATE):在伺服器更新資源(客戶端提供改變的屬性)。
  • DELETE(DELETE):從伺服器刪除資源。

其中

1 刪除資源 必須DELETE 方法 2 建立新的資源 必須 使用 POST 方法 3 更新資源 應該 使用 PUT 方法 4 獲取資源資訊 必須 使用 GET 方法

針對每一個端點來說,下面列出所有可行的 HTTP 動詞和端點的組合

請求方法 URL 描述
GET /zoos 列出所有的動物園(ID和名稱,不要太詳細)
POST /zoos 新增一個新的動物園
GET /zoos/{zoo} 獲取指定動物園詳情
PUT /zoos/{zoo} 更新指定動物園(整個物件)
PATCH /zoos/{zoo} 更新動物園(部分物件)
DELETE /zoos/{zoo} 刪除指定動物園
GET /zoos/{zoo}/animals 檢索指定動物園下的動物列表(ID和名稱,不要太詳細)
GET /animals 列出所有動物(ID和名稱)。
POST /animals 新增新的動物
GET /animals/{animal} 獲取指定的動物詳情
PUT /animals/{animal} 更新指定的動物(整個物件)
PATCH /animals/{animal} 更新指定的動物(部分物件)
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} 從某個動物園解僱一名員工

Filtering

如果記錄數量很多,伺服器不可能都將它們返回給使用者。API 應該 提供引數,過濾返回結果。下面是一些常見的引數。

  • ?limit=10:指定返回記錄的數量
  • ?offset=10:指定返回記錄的開始位置。
  • ?page=2&per_page=100:指定第幾頁,以及每頁的記錄數。
  • ?sortby=name&order=asc:指定返回結果按照哪個屬性排序,以及排序順序。
  • ?animal_type_id=1:指定篩選條件

所有 URL 引數 必須 是全小寫,必須 使用下劃線型別的引數形式。

經常使用的、複雜的查詢 應該 標籤化,降低維護成本。如

GET /trades?status=closed&sort=sortby=name&order=asc

# 可為其定製快捷方式
GET /trades/recently_closed
複製程式碼

Authentication

應該 使用 OAuth2.0 的方式為 API 呼叫者提供登入認證。必須 先通過登入介面獲取 Access Token 後再通過該 token 呼叫需要身份認證的 API

Oauth 的端點設計示列

  • RFC 6749 /token
  • Twitter /oauth2/token
  • Fackbook /oauth/access_token
  • Google /o/oauth2/token
  • Github /login/oauth/access_token
  • Instagram /oauth/authorize

客戶端在獲得 access token 的同時 必須 在響應中包含一個名為 expires_in 的資料,它表示當前獲得的 token 會在多少 後失效。

{
    "access_token": "token....",
    "token_type": "Bearer",
    "expires_in": 3600
}
複製程式碼

客戶端在請求需要認證的 API 時,必須 在請求頭 Authorization 中帶上 access_token

Authorization: Bearer token...
複製程式碼

當超過指定的秒數後,access token 就會過期,再次用過期/或無效的 token 訪問時,服務端 應該 返回 invalid_token 的錯誤或 401 錯誤碼。

HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
    "error": "invalid_token"
}
複製程式碼

Laravel 開發中,應該 使用 JWT 來為管理你的 Token,並且 一定不可api 中介軟體中開啟請求 session

Response

所有的 API 響應,必須 遵守 HTTP 設計規範,必須 選擇合適的 HTTP 狀態碼。一定不可 所有介面都返回狀態碼為 200HTTP 響應,如:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": 0,
    "msg": "success",
    "data": {
        "username": "username"
    }
}
複製程式碼

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": -1,
    "msg": "該活動不存在",
}
複製程式碼

下表列舉了常見的 HTTP 狀態碼

狀態碼 描述
1xx 代表請求已被接受,需要繼續處理
2xx 請求已成功,請求所希望的響應頭或資料體將隨此響應返回
3xx 重定向
4xx 客戶端原因引起的錯誤
5xx 服務端原因引起的錯誤

只有來自客戶端的請求被正確的處理後才能返回 2xx 的響應,所以當 API 返回 2xx 型別的狀態碼時,前端 必須 認定該請求已處理成功。

必須強調的是,所有 API 一定不可 返回 1xx 型別的狀態碼。當 API 發生錯誤時,必須 返回出錯時的詳細資訊。目前常見返回錯誤資訊的方法有兩種:

1、將錯誤詳細放入 HTTP 響應首部;

X-MYNAME-ERROR-CODE: 4001
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication
複製程式碼

2、直接放入響應實體中;

HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:02:59 GMT
Connection: keep-alive

{"error_code":40100,"message":"Unauthorized"}
複製程式碼

考慮到易讀性和客戶端的易處理性,我們 必須 把錯誤資訊直接放到響應實體中,並且錯誤格式 應該 滿足如下格式:

{
    "message": "您查詢的資源不存在",
    "error_code": 404001
}
複製程式碼

其中錯誤碼(error_code必須HTTP 狀態碼對應,也方便錯誤碼歸類,如:

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:15:52 GMT
Connection: keep-alive

{"error_code":429001,"message":"你操作太頻繁了"}
複製程式碼
HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:19:27 GMT
Connection: keep-alive

{"error_code":403002,"message":"使用者已禁用"}
複製程式碼

應該 在返回的錯誤資訊中,同時包含面向開發者和麵向使用者的提示資訊,前者可方便開發人員除錯,後者可直接展示給終端使用者檢視如:

{
    "message": "直接展示給終端使用者的錯誤資訊",
    "error_code": "業務錯誤碼",
    "error": "供開發者檢視的錯誤資訊",
    "debug": [
        "錯誤堆疊,必須開啟 debug 才存在"
    ]
}
複製程式碼

下面詳細列舉了各種情況 API 的返回說明。

200 ok

200 狀態碼是最常見的 HTTP 狀態碼,在所有 成功GET 請求中,必須 返回此狀態碼。HTTP 響應實體部分 必須 直接就是資料,不要做多餘的包裝。

錯誤示例:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "user": {
        "id":1,
        "nickname":"fwest",
        "username": "example"
    }
}
複製程式碼

正確示例:

1、獲取單個資源詳情

{
    "id": 1,
    "username": "godruoyi",
    "age": 88,
}
複製程式碼

2、獲取資源集合

[
    {
        "id": 1,
        "username": "godruoyi",
        "age": 88,
    },
    {
        "id": 2,
        "username": "foo",
        "age": 88,
    }
]
複製程式碼

3、額外的媒體資訊

{
    "data": [
        {
            "id": 1,
            "avatar": "https://lorempixel.com/640/480/?32556",
            "nickname": "fwest",
            "last_logined_time": "2018-05-29 04:56:43",
            "has_registed": true
        },
        {
            "id": 2,
            "avatar": "https://lorempixel.com/640/480/?86144",
            "nickname": "zschowalter",
            "last_logined_time": "2018-06-16 15:18:34",
            "has_registed": true
        }
    ],
    "meta": {
        "pagination": {
            "total": 101,
            "count": 2,
            "per_page": 2,
            "current_page": 1,
            "total_pages": 51,
            "links": {
                "next": "http://api.example.com?page=2"
            }
        }
    }
}
複製程式碼

其中,分頁和其他額外的媒體資訊,必須放到 meta 欄位中。

201 Created

當伺服器建立資料成功時,應該 返回此狀態碼。常見的應用場景是使用 POST 提交使用者資訊,如:

  • 新增了新使用者
  • 上傳了圖片
  • 建立了新活動

等,都可以返回 201 狀態碼。需要注意的是,你可以選擇在使用者建立成功後返回新使用者的資料

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:13:40 GMT
Connection: keep-alive

{
    "id": 1,
    "avatar": "https:\/\/lorempixel.com\/640\/480\/?32556",
    "nickname": "fwest",
    "last_logined_time": "2018-05-29 04:56:43",
    "created_at": "2018-06-16 17:55:55",
    "updated_at": "2018-06-16 17:55:55"
}
複製程式碼

也可以返回一個響應實體為空的 HTTP Response 如:

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:12:20 GMT
Connection: keep-alive
複製程式碼

這裡我們 應該 採用第二種方式,因為大多數情況下,客戶端只需要知道該請求操作成功與否,並不需要返回新資源的資訊。

202 Accepted

該狀態碼錶示伺服器已經接受到了來自客戶端的請求,但還未開始處理。常用簡訊傳送、郵件通知、模板訊息推送等這類很耗時需要佇列支援的場景中;

返回該狀態碼時,響應實體 必須 為空。

HTTP/1.1 202 Accepted
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:25:15 GMT
Connection: keep-alive
複製程式碼

204 No Content

該狀態碼錶示響應實體不包含任何資料,其中:

  • 在使用 DELETE 方法刪除資源 成功 時,必須 返回該狀態碼
  • 使用 PUTPATCH 方法更新資料 成功 時,也 應該 返回此狀態碼
HTTP/1.1 204 No Content
Server: nginx/1.11.9
Date: Sun, 24 Jun 2018 09:29:12 GMT
Connection: keep-alive
複製程式碼

3xx 重定向

所有 API 一定不可 返回 3xx 型別的狀態碼。因為 3xx 型別的響應格式一般為下列格式:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 09:41:50 GMT
Location: https://example.com
Connection: keep-alive

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url=https://example.com" />

        <title>Redirecting to https://example.com</title>
    </head>
    <body>
        Redirecting to <a href="https://example.com">https://example.com</a>.
    </body>
</html>
複製程式碼

API 一定不可 返回純 HTML 結構的響應;若一定要使用重定向功能,應該 返回一個響應實體為空的 3xx 響應,並在響應頭中加上 Location 欄位:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:52:50 GMT
Location: https://godruoyi.com
Connection: keep-alive
複製程式碼

400 Bad Request

由於明顯的客戶端錯誤(例如,請求語法格式錯誤、無效的請求、無效的簽名等),伺服器 應該 放棄該請求。

當伺服器無法從其他 4xx 型別的狀態碼中找出合適的來表示錯誤型別時,都 必須 返回該狀態碼。

HTTP/1.1 400 Bad Request
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:22:36 GMT
Connection: keep-alive

{"error_code":40000,"message":"無效的簽名"}
複製程式碼

401 Unauthorized

該狀態碼錶示當前請求需要身份認證,以下情況都 必須 返回該狀態碼。

  • 未認證使用者訪問需要認證的 API
  • access_token 無效/過期

客戶端在收到 401 響應後,都 應該 提示使用者進行下一步的登入操作。

HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
WWW-Authenticate: JWTAuth
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:17:02 GMT
Connection: keep-alive

"message":"Token Signature could not be verified.","error_code": "40100"}
複製程式碼

403 Forbidden

該狀態碼可以簡單的理解為沒有許可權訪問該請求,伺服器收到請求但拒絕提供服務。

如當普通使用者請求操作管理員使用者時,必須 返回該狀態碼。

HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:05:34 GMT
Connection: keep-alive

{"error_code":40301,"message":"許可權不足"}
複製程式碼

404 Not Found

該狀態碼錶示使用者請求的資源不存在,如

  • 獲取不存在的使用者資訊 (get /users/9999999)
  • 訪問不存在的端點

必須 返回該狀態碼,若該資源已永久不存在,則 應該 返回 410 響應。

405 Method Not Allowd

當客戶端使用的 HTTP 請求方法不被伺服器允許時,必須 返回該狀態碼。

如客戶端呼叫了 POST 方法來訪問只支援 GET 方法的 API

該響應 必須 返回一個 Allow 頭資訊用以表示出當前資源能夠接受的請求方法的列表。

HTTP/1.1 405 Method Not Allowed
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Allow: GET, HEAD
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:30:57 GMT
Connection: keep-alive

{"message":"405 Method Not Allowed","error_code": 40500}
複製程式碼

406 Not Acceptable

API 在不支援客戶端指定的資料格式時,應該返回此狀態碼。如支援 JSONXML 輸出的 API 被指定返回 YAML 格式的資料時。

Http 協議一般通過請求首部的 Accept 來指定資料格式

408 Request Timeout

客戶端請求超時時 必須 返回該狀態碼,需要注意的時,該狀態碼錶示 客戶端請求超時,在涉及第三方 API 呼叫超時時,一定不可 返回該狀態碼。

409 Gonfilct

該狀態碼錶示因為請求存在衝突無法處理。如通過手機號碼提供註冊功能的 API,當使用者提交的手機號已存在時,必須 返回此狀態碼。

HTTP/1.1 409 Conflict
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:19:04 GMT
Connection: keep-alive

{"error_code":40900,"message":"手機號已存在"}
複製程式碼

410 Gone

404 類似,該狀態碼也表示請求的資源不存在,只是 410 狀態碼進一步表示所請求的資源已不存在,並且未來也不會存在。在收到 410 狀態碼後,客戶端 應該 停止再次請求該資源。

413 Request Entity Too Large

該狀態碼錶示伺服器拒絕處理當前請求,因為該請求提交的實體資料大小超過了伺服器願意或者能夠處理的範圍。

此種情況下,伺服器可以關閉連線以免客戶端繼續傳送此請求。

如果這個狀況是臨時的,伺服器 應該 返回一個 Retry-After 的響應頭,以告知客戶端可以在多少時間以後重新嘗試。

414 Request-URI Too Long

該狀態碼錶示請求的 URI 長度超過了伺服器能夠解釋的長度,因此伺服器拒絕對該請求提供服務。

415 Unsupported Media Type

通常表示伺服器不支援客戶端請求首部 Content-Type 指定的資料格式。如在只接受 JSON 格式的 API 中放入 XML 型別的資料並向伺服器傳送,都 應該 返回該狀態碼。

該狀態碼也可用於如:只允許上傳圖片格式的檔案,但是客戶端提交媒體檔案非法或不是圖片型別,這時 應該 返回該狀態碼:

HTTP/1.1 415 Unsupported Media Type
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:09:40 GMT
Connection: keep-alive

{"error_code":41500,"message":"不允許上傳的圖片格式"}
複製程式碼

429 Too Many Request

該狀態碼錶示使用者請求次數超過允許範圍。如 API 設定為 60次/分鐘,當使用者在一分鐘內請求次數超過 60 次後,都 應該 返回該狀態碼。並且也 應該 在響應首部中加上下列頭部:

X-RateLimit-Limit: 10 請求速率(由應用設定,其單位一般為小時/分鐘等,這裡是 10次/5分鐘)
X-RateLimit-Remaining: 0 當前剩餘的請求數量
X-RateLimit-Reset: 1529839462 重置時間
Retry-After: 120 下一次訪問應該等待的時間(秒)
複製程式碼

列子

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1529839462
Retry-After: 290
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 11:19:32 GMT
Connection: keep-alive

{"message":"You have exceeded your rate limit.","error_code":42900}
複製程式碼

必須 為所有的 API 設定 Rate Limit 支援。

500 Internal Server Error

該狀態碼 必須 在伺服器出錯時丟擲,對於所有的 500 錯誤,都 應該 提供完整的錯誤資訊支援,也方便跟蹤除錯。

503 Service Unavailable

該狀態碼錶示伺服器暫時處理不可用狀態,當伺服器需要維護或第三方 API 請求超時/不可達時,都 應該 返回該狀態碼,其中若是主動關閉 API 服務,應該在返回的響應首部加上 Retry-After 頭部,表示多少秒後可以再次訪問。

HTTP/1.1 503 Service Unavailable
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:56:20 GMT
Retry-After: 60
Connection: keep-alive

{"error_code":50300,"message":"服務維護中"}
複製程式碼

其他 HTTP 狀態碼請參考 HTTP 狀態碼- 維基百科

版權宣告

文章釋出於 二楞徐的閒談雜魚,自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證

建議參考

restful-api-design-references

Principles of good RESTful API Design(譯)

理解 RESTful 架構

RESTful API 設計指南

HTTP 狀態碼- 維基百科

相關文章