Restful API設計規范及實戰【說的比較清楚了】


Restful API設計規范及實戰

 

Restful API的概念在此就不費口舌了,博友們網上查哈定義文章很多,直入正題吧:

首先拋出一個問題:
判斷id為 用戶下,名稱為 使命召喚14(COD14) 的產品是否存在(話說我還是很喜歡玩類似二戰的使命召喚這款額,題外話...)?如果這個問題出現在 MVC 項目中,我想我們一般會這樣設計:

api/products/isexist/{userId}/{productName}

我想你應該發現一些問題了,這種寫法完全是 MVC 的方式,但並不適用於 WebAPI,主要有三個問題:
Route 定義混亂,完全違背 REST API URI 的一些設計原則。Action 命名不恰當。bool 返回值不合適。對於上面的三個問題,我們分別來探討下。

1. URI 設計首先,我們知道在 REST API 中,URI 代表的是一種資源,它的設計要滿足兩個基本要求,第一名詞而非動詞,第二要能清晰表達出資源的含義,

換句話說就是,從一個 URI 中,你可以很直接明了的知道訪問的資源是什么,我們再來看我們設計的 URI:

api/products/isExist/{userId}/{productName}

這是神馬玩意啊???這種設計完全違背 URI 原則,首先,我們先梳理一下,我們想要請求的資源是什么?沒錯,是產品(Products),但這個產品是某一個用戶下的,

所以用戶和產品有一個上下級關系,訪問產品首先得訪問用戶,這一點要在 URI 中進行體現,其次,我們是獲取產品?還是判斷產品是否存在?這個概念是不同的,

產品的唯一標識和用戶一樣,都是 id,在 URI 的一般設計中,如果要訪問某一唯一標識下的資源(比如 id 為 1 的 product),會這樣進行設計:

api/products/{id}

HttpClient 請求中會用 HttpGet 方法(api/products/1),這樣我們就可以獲得一個 id 為 1 的 product,但現在的場景是,獲取產品不通過唯一標識,而是通過產品名稱,難道我們要這樣設計:

api/products/{productName}

咋看之下,這樣好像設計也沒毛病啊,但總覺得有些不對勁,比如如果再加一個產品大小,難道要改成這樣:api/products/{productName}/{productSize},這種設計完全是不恰當的,上面說到,

URI 代表的是一種資源,通過 URI 獲取資源的唯一方式是通過資源的唯一標識,除此之外的獲取都可以看作是對資源的查詢(Query),所以,針對我們的應用場景,URI 的設計應該是這樣(正確):

格式標准: api/users/{userId}/products:
示 例  : api/users/1/products?productName=使命召喚COD14

上面的 URI 清晰明了的含義:查詢 id 為 1 用戶下名稱為 COD14 的產品。

2. Action 命名對於 IsExist 的命名,如果沒有很強的強迫症,其實也是可以接受的,因為 WebAPI 的 URI 並不會像 MVC 的 Route 設計那樣,在訪問的時候,URL 一般會默認 Action 的名字,所以,

在 WebAPI Action 設計的時候,會在 Action 前面加一個 Route 屬性,用來配置 URI,也就是說每一個 Action 操作會對應一個 URI 請求操作,這個請求操作也就是 HTTP 的常用方法。

如果我們想把 IsExist 改掉,那用什么命名會好些呢?Action 的命名和 HTTP 方法一樣,比如 Get 就是 Get,而不是 GetById,Get 是動詞,表示它對資源的一種操作,具體是通過什么進行操作?

在參數中可以很直觀的進行反應,一般會在 HelpPage 中進行注釋說明。

IsExist 的含義還是判斷資源是否存在,其本質上來說就是去獲取一個資源,也就是 Get 操作,所以,在 WebAPI Action 中對這樣的命名,我們直接使用 Get 會好一下,或者使用 Exist。

3. 請求返回bool 一般是用在項目方法中的返回值,如果用在 HTTP 請求中,就不是很恰當了。

上面只是介紹了簡單的設計場景,回歸正題,繼續:

設計方法及原則:

1. 使用HTTP方法:

HTTP1.1的規范定義了8個動詞,然而HTTP作為一個規范並沒有被嚴格地遵守着,在大多數情況下POST是可以完成除任何種類的請求,所以現在很多的API設計都是只是用GET和POST來調用API,

在這種情況下,一般的做法是使用GET用來獲取資源,其他的行為都是用POST來完成,而為了區別不同的行為,往往在API的Uri中加入動詞,如百度推送的如下API:

POST ] /rest/3.0/app/del_tag

功能

刪除一個已存在的tag

參數

參數名 類型 必需 限制 描述
tag string 1~128字節,但不能為‘default’ 標簽名稱

返回值

名稱 類型 描述
tag string 標簽名稱
result number 狀態 0:創建成功; 1:創建

更清晰API設計的可能會使用GET POST PUT DELETE四種方法分別代表“查詢、添加、更新、刪除”等四個動作,這在概念上是符合HTTP規范的,如Google的如下API:
DELETE https://www.googleapis.com/bigquery/v2/projects/datasets/?key={YOUR_API_KEY}

在我看來,沒有絕對的好與不好。如果使用第一種方法,那么只要保證Uri的語義清晰,其實和使用第二種方法沒有太大的區別。

2. Uri格式:

Uri在REST中標識了一個資源,但是在具體的API設計中,往往不能做到完全的對於資源的映射,本文中的設計將參考比較流行的Uri設計,大致有這么幾條:

  • Uri的根(root/)應當能夠標識這是一個RESTful API,以與同目錄下其他可能存在的資源進行區分。
  • 緊接着Uri的根,應當標識當前API的版本號。
  • 如果方法是POST或者PUT,盡量避免使用URL編碼的參數,盡量保持Uri的干凈。
  • 如果方法是DELETE,Uri應當完全標識了需要刪除的對象或者對象的集合,避免在DELETE的請求中使用其他參數,因為某些服務器可能會丟棄伴隨着DELETE發送的內容。

這里還是拿行業標桿Google的開放API來舉例:

3. 固定返回碼

REST的大部分實現都是一個基於HTTP的,那么自然而然就少不了與返回碼打交道,然而不幸的是,HTTP的返回碼定義的看起來十分隨意,很多錯誤信息語意不詳,而且在實際的開發中,

API的使用者需要處理鏈路的問題(如超時等)、種類繁多的HTTP返回碼、和實際的返回內容,不堪其繁瑣。更嚴重的是,這些返回碼大多最終依賴於服務端開發者的具體實現,

而這種看似約定的東西分別在客戶端和服務端開發者眼中的含義可能相去甚遠。

那么從需求入手,我們在使用RESTful API時需要使用返回碼的原因大致是這樣的:客戶端在調用一個API之后,需要在接收到的反饋必須要能夠標識這次調用是否成功,

如果不成功,客戶端需要拿到失敗的原因。我們可以在API設計時作一個小小的約定,就能完美的滿足以上需求了。

服務端在成功接收到客戶端的請求之后,永遠返回200,具體成功與否及進一步的信息放入返回的內容。

在這個場景中,如果是鏈路出了問題或者服務器錯誤等(返回碼不等於200),客戶端很容易就能捕獲這個錯誤,如果鏈路沒問題,那么出錯與否在獲取到的反饋內容中會有詳細的描述。

4. 固定返回結構

現在越來越多的API設計會使用JSON來傳遞數據,本文中的設計也將使用JSON。JSON-RPC是一個基於JSON的廣為人知的設計簡潔的RPC規范,這里將借鑒JSON-RPC的響應對象的設計。

JSON-RPC中服務端響應對象的設計的基本理念是,只要調用成功,服務端必須響應數據,而響應數據的格式在任何情況下都應當是一致的,JSON-RPC的響應格式是這么設計的:

復制代碼
{"jsonrpc": "2.0", "result": 19, "id": 1}

{
    "jsonrpc": "2.0", 
    "error": 
        {
            "code": -23400, 
            "message": "Invalid Request"
        }, 
    "id": null
}
復制代碼

由於JSON-RPC的目標是建立一個通用的規范,所以響應格式的設計還是有些復雜,我們可以只取其中它對於error對象的設計,所有返回的格式必須是這樣的:

{
    "code": -23400, 
    "message": "Invalid Request”, 
    “data”:{ }
}

這種格式的設計在許多大公司的開放API中也較為常見,比如作為行業標桿的Google,在調用Google開放平台的某API后獲取到的錯誤數據如下,其設計思想與這里討論的這種返回格式的思想如出一轍。

復制代碼
{"error": {
    "errors": [
            {
                "domain": "global",
                "reason": "required",
                "message": "Login Required",
                "locationType": "header",
                "location": "Authorization"
            }
        ],
    "code": 401,
    "message": "Login Required"
    }
}
復制代碼
綜上所述,我們這里所探討的API設計應該是這樣的:
  1. 所有API的Uri為基於HTTP的名詞性短語,用來代表一種資源。

  2. Uri格式如文中所述。

  3. 使用GET POST PUT DELETE四種方法分別代表對資源的“查詢、添加、更新、刪除”。

  4. 服務端接收到客戶端的請求之后,統一返回200,如果客戶端獲取到的返回碼不是200,代表鏈路上某一個環節出了問題。

  5. 服務端所有的響應格式為:

     {   
         “code”: -23400, 
         “message”: “Invalid Request”, 
         “data”:{ }
     }

    他們的含義分別代表:

    • code為0代表調用成功,其他會自定義的錯誤碼;
    • message表示在API調用失敗的情況下詳細的錯誤信息,這個信息可以由客戶端直接呈現給用戶,否則為空;
    • data表示服務端返回的數據,具體格式由服務端自定義,API調用錯誤為空

還沒完。。。。。這可能寫的又臭又長...但是下面是回歸重點額,無論在面試還是處於自己開發項目中,restful api的設計規范還是很有必要知曉滴。繼續我的廢話:

使用的名詞而不是動詞

不應該使用動詞:

/getAllResources 
/createNewResources 
/deleteAllResources

GET方法和查詢參數不能改變資源狀態:

如果要改變資源的狀態,使用PUT、POST、DELETE。下面是錯誤的用GET方法來修改user的狀態:

GET /users/211?activate
GET /users/211/activate

Rest的核心原則是將你的API拆分為邏輯上的資源。這些資源通過HTTP被操作(GET,POST,PUT,DELETE,關於Http的幾種狀態,請參考我之前寫的一篇:https://www.cnblogs.com/phpper/p/9127553.html)

我們定義資源ticket、user、group:

復制代碼
GET /tickets # 獲取ticket列表

GET /tickets/12 # 查看某個具體的ticket

POST /tickets # 新建一個ticket

PUT /tickets/12 #新建ticket 12

DELETE /tickets/12 # 刪除ticket 12
復制代碼

只需要一個endpoint:/tickets,再也沒有其他什么命名規則和url規則了。

一個可以遵循的規則是:雖然看起來使用復數來描述某一個資源看起來特別扭,但是統一所有的endpoint,使用復數使得你的URL更加規整。這讓API使用者更加容易理解,對開發者來說也更容易實現。

處理關聯:

1
2
3
4
5
6
7
8
9
GET /tickets/12/messages # 獲取ticket 12的message列表
 
GET /tickets/12/messages/5 #獲取ticket 12的message 5
 
POST /tickets/12/messages 創建ticket 12的一個message
 
PUT /tickets/12/messages/5 更新ticket 12的message 5
 
DELETE /tickets/12/messages/5 刪除ticket 12的message 5

避免層級過深的URI

/ 在url中表達層級,用於按實體關聯關系進行對象導航,一般根據id導航。

過深的導航容易導致url膨脹,不易維護,如 GET /zoos/1/areas/3/animals/4,盡量使用查詢參數代替路勁中的實體導航,如GET /animals?zoo=1&area=3

結果過濾,排序,搜索

url最好越簡短越好,對結果過濾、排序、搜索相關的功能都應該通過參數實現。

過濾:例如你想限制GET /tickets 的返回結果:只返回那些open狀態的ticket, GET /tickets?state=open 這里的state就是過濾參數。

排序:和過濾一樣,一個好的排序參數應該能夠描述排序規則,而不和業務相關。復雜的排序規則應該通過組合實現。排序參數通過 , 分隔,排序參數前加 - 表示降序排列。

  • GET /tickets?sort=-priority #獲取按優先級降序排列的ticket列表

  • GET /tickets?sort=-priority,created_at #獲取按優先級降序排列的ticket列表,在同一個優先級內,先創建的ticket排列在前面。

搜索:有些時候簡單的排序是不夠的。我們可以使用搜索技術來實現

  • GET /tickets?q=return&state=open&sort=-priority,create_at # 獲取優先級最高且打開狀態的ticket,而且包含單詞return的ticket列表。

限制API返回值的域

有時候API使用者不需要所有的結果,在進行橫向限制的同時(例如值返回API結果的前十個),還應該可以進行縱向限制,並且這個功能能有效的提高網絡帶寬使用率和速度。可以使用fields查詢參數來限制返回的域例如:

  • GET /tickets?fields=id,subject,customer_name,updated_at&state=open&sort=-updated_at

Response不要包裝

response 的 body直接就是數據,不要做多余的包裝。錯誤實例:

{
    "success":true,
    "data":{"id":1, "name":"周伯通"}
}

更新和創建操作應該返回資源

在POST操作以后,返回201created 狀態碼,並且包含一個指向新資源的url作為返回頭。

命名方式

是蛇形命名還是駝峰命名?如果使用json那么最好的應該是遵守JavaScript的命名方法-駝峰命名法。Java、C# 使用駝峰,python、ruby使用蛇形。

默認使用pretty print格式,開啟gzip

開啟pretty print返回結果會更加友好易讀,而且額外的傳輸也可以忽略不計。如果忘了使用gzip那么傳輸效率將會大大減少,損失大大增加。

GitHub v3S實踐經驗

1.Current Version(當前版本)

通過Accept字段來區分app版本號,而不是在url中嵌入版本號(比如迭代的v1,v2,v3等): 

Accept: application/vnd.github.v3+json

2.Schema(計划)

Summary Representation

當你請求獲取某一資源的列表時,響應僅返回資源的屬性子集。有些屬性對API來說代價是非常高的,出於性能的考慮,會排除這些屬性。要獲取這些屬性,請求"detailed" representation。

Example:當你獲取倉庫的列表時,你獲得的是每個倉庫的summary representation。

GET /orgs/octokit/repos

Detailed Representation(詳細描述)

當你獲取一個單獨的資源時,響應會返回這個資源的所有屬性。

Example:當你獲取一個單獨的倉庫,你會獲得這個倉庫的detailed representation。

GET /repos/octokit/octokit.rb

3.Parameters(參數)

許多API都帶有可選參數。對於GET請求,任何不作為路徑構成部分的參數都可以通過HTTP查詢參數傳入。

GET https://api.github.com/repos/vmg/redcarpet/issues?state=closed

在這個例子中,'vmg' 和 'redcarpet' 作為 :owner 和 :repo 的參數,而 :state 作為查詢參數。

對於POST、PATCH、PUT和DELETE的請求,不包含在URL中的參數需要編碼成JSON傳遞,且 Content-Type為 'application/json'。

Root Endpoint(根節點)

你可以對根節點GET請求,獲取根節點下的所有API分類。

Client Errors(客戶端錯誤)

有三種可能的客戶端錯誤,在接收到請求體時:

發送非法JSON會返回 400 Bad Request.

HTTP/1.1 400 Bad Request
Content-Length: 35

{"message":"Problems parsing JSON"}

發送錯誤類型的JSON值會返回 400 Bad Request.

HTTP/1.1 400 Bad Request
Content-Length: 40

{"message":"Body should be a JSON object"}

發送無效的值會返回 422 Unprocessable Entity.

復制代碼
HTTP/1.1 422 Unprocessable Entity
Content-Length: 149

{
      "message": "Validation Failed",
      "errors": [
    {
      "resource": "Issue",
      "field": "title",
      "code": "missing_field"
    }
  ]
}
復制代碼

我們可以告訴發生了什么錯誤,下面是一些可能的驗證錯誤碼:

Error Name Description
missing 資源不存在
missing_field 資源必需的域沒有被設置
invalid 域的格式非法
already_exists 另一個資源的域的值和此處的相同,這會發生在資源有唯一的鍵的時候

HTTP Redirects(HTTP重定向)

API v3在合適的地方使用HTTP重定向。客戶端應該假設任何請求都會導致重定向。重定向在響應頭中有一個 Location 的域,此域包含了資源的真實位置。

HTTP Verbs(HTTP動詞)

API v3力爭使用正確的HTTP動詞來表示每次請求。

Verb Description
HEAD 對任何資源僅請求頭信息
GET 獲取資源
POST 創建資源
PATCH 使用部分的JSON數據更新資源
PUT 取代資源或資源集合
DELETE 刪除資源

Hypermedia(超媒體)

很多資源有一個或者更多的 *_url 屬性指向其他資源。這意味着服務端提供明確的URL,這樣客戶端就不必要自己構造URL了。

Pagination(分頁)

請求資源列表時會進行分頁,默認每頁30個。當你請求后續頁的時候可以使用 ?page 參數。對於某些資源,你可以通過參數 ?per_page自定義每頁的大小。

curl 'https://api.github.com/user/repos?page=2&per_page=100'

需要注意的一點是,頁碼是從1開始的,當省略參數 ?page 時,會返回首頁。

Basics of Pagination(分頁基礎)

關於分頁的其他相關信息在響應的頭信息的 Link 里提供。比如,去請求一個搜索的API,查找Mozilla的項目中哪些包含詞匯addClass :

curl -I "https://api.github.com/search/code?q=addClass+user:mozilla"

頭信息中Link字段如下:

Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2>; rel="next", <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last"

rel="next" 表示下一頁是 page=2。也就是說,默認情況下所有的分頁請求都是從首頁開始。rel="last" 提供更多信息,表示最后一頁是34。即我們還有33頁的信息包含addClass。

總之,我們應該依賴於Link提供的信息,而不要嘗試自己去猜或者構造URL。

Navigating through the pages

既然已經知道會接收多少頁面,我們可以通過頁面導航來消費結果。我們可以通過傳遞一個page參數,例如跳到14頁:

curl -I "https://api.github.com/search/code?q=addClass+user:mozilla&page=14"

這是頭信息中Link字段:

Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=15>; rel="next",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=1>; rel="first",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=13>; rel="prev"

我們會獲得更多的信息,rel="first"表示首頁,rel="prev"表示前一頁的頁碼。通過這些信息,我們可以構造一個UI界面讓用戶在first、previous、next、last之間進行跳轉。

Rate Limiting(速率限制)

對於認證的請求,可以每小時最多請求5000次。對於沒有認證的請求,限制在每小時60次請求。

檢查返回的HTTP頭,可以看到當前的速率限制:

復制代碼
curl -i https://api.github.com/users/whatever   
                                                   
HTTP/1.1 200 OK
Server: GitHub.com
Date: Thu, 27 Oct 2016 03:05:42 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 1219
Status: 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 48
X-RateLimit-Reset: 1477540017
復制代碼

header頭信息告訴你當前的速率限制狀態:

Header Name Description
X-RateLimit-Limit 當前用戶被允許的每小時請求數
X-RateLimit-Remaining 在當前發送窗口內還可以發送的請求數
X-RateLimit-Reset 按當前速率發送后,發送窗口重置的時間

一旦你超過了發送速率限制,你會收到一個錯誤響應:

復制代碼
HTTP/1.1 403 Forbidden
Date: Tue, 20 Aug 2013 14:50:41 GMT
Status: 403 Forbidden
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1377013266

{
       "message": "API rate limit exceeded for xxx.xxx.xxx.xxx. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)",
       "documentation_url": "https://developer.github.com/v3/#rate-limiting"
}
復制代碼

User Agent Required

所有的API請求必須包含一個有效的 User-Agent 頭。請求頭不包含User-Agent的請求會被拒絕。

Conditional requests

大多數響應都會返回一個 ETag 頭。很多響應也會返回一個 Last-Modified 頭。你可以使用這些頭信息對這些資源進行后續請求,分別使用 If-None-Match 和 If-Modified-Since頭。如果資源沒有發生改變,服務器端會返回 304 Not Modified

Enchant REST API 實踐經驗

Requests

Limited HTTP Clients

如果你使用的HTTP客戶端不支持PUT、PATCH、DELETE方法,發送一個POST請求,頭信息里包含X-HTTP-Method-Override字段,它的值是實際需要的動詞。

$ curl -u email:password https://site.enchant.com/api/v1/users/543abc \
    -X POST \
    -H "X-HTTP-Method-Override: DELETE"

Rate Limiting

所有響應的頭部包含描述當前限流狀態的字段:

Rate-Limit-Limit: 100
Rate-Limit-Remaining: 99
Rate-Limit-Used: 1
Rate-Limit-Reset: 20
  • Rate-Limit-Limit - 當前時間段內允許的總的請求數

  • Rate-Limit-Remaining - 當前時間段內還剩余的請求數

  • Rate-Limit-Used - 本次所使用的請求數

  • Rate-Limit-Reset - 重置所需秒數

如果速率限制被打破,API會返回 429 Too Many Requests 的狀態碼。在這種情況下,你的應用不應該再發送任何請求直到 Rate-Limit-Reset 所規定的時間過去。

Field Filtering(字段過濾)

你可以自己限制響應返回的域。只需要你傳遞一個 fields 參數,用逗號分隔所需要的域,比如:

GET /api/v1/users?fields=id,first_name

Counting

所有返回一個集合的URL,都會提供count統計所有結果的個數。要獲取count值需要加一個 count=true 的參數。count會在消息頭中的Total-Count 字段中返回。

GET /api/v1/tickets?count=true

復制代碼
200 OK
Total-Count: 135
Rate-Limit-Limit: 100
Rate-Limit-Remaining: 98
Rate-Limit-Used: 2
Rate-Limit-Reset: 20
Content-Type: application/json
復制代碼

count表示所有現存結果的數量,而不是此次響應返回的結果的數量。

Enveloping

如果你的HTTP客戶端難以讀取狀態碼和頭信息,我們可以將所有都打包進響應消息體中。我們只需要傳遞參數 envelope=true,而API會始終返回200的HTTP狀態碼。真正的狀態碼、頭信息和響應都在消息體中。

GET /api/v1/users/does-not-exist?envelope=true
200 OK
復制代碼
{
      "status": 404,
      "headers": {
    "Rate-Limit-Limit": 100,
    "Rate-Limit-Remaining": 50,
    "Rate-Limit-Used": 0,
    "Rate-Limit-Reset": 25
  },
  "response": {
    "message": "Not Found"
  }
}
復制代碼

其他如 分頁、排序等,enchant的設計規范和GitHub v3大致相同。有興趣的朋友可以了解下相關的資料。

另外發現一款提升開發效率的接口管理工具,體驗很好,涵蓋文檔管理、團隊協作以及接口測試,eoLinker接口管理平台:https://www.eolinker.com,感興趣的朋友可以體驗哈。

無論從事什么行業,只要做好兩件事就夠了,一個是你的專業、一個是你的人品,專業決定了你的存在,人品決定了你的人脈,剩下的就是堅持,用善良專業和真誠贏取更多的信任。不忘初心 方得始終!
 
分類:  php雜記


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM