Restful API的概念在此就不費口舌了,博友們網上查哈定義文章不少,直入正題吧:php
首先拋出一個問題:
判斷id爲 用戶下,名稱爲 使命召喚14(COD14) 的產品是否存在(話說我仍是很喜歡玩相似二戰的使命召喚這款額,題外話...)?若是這個問題出如今 MVC 項目中,我想咱們通常會這樣設計:html
api/products/isexist/{userId}/{productName}
我想你應該發現一些問題了,這種寫法徹底是 MVC 的方式,但並不適用於 WebAPI,主要有三個問題:
Route 定義混亂,徹底違背 REST API URI 的一些設計原則。Action 命名不恰當。bool 返回值不合適。對於上面的三個問題,咱們分別來探討下。
1. URI 設計首先,咱們知道在 REST API 中,URI 表明的是一種資源,它的設計要知足兩個基本要求,第一名詞而非動詞,第二要能清晰表達出資源的含義,python
換句話說就是,從一個 URI 中,你能夠很直接明瞭的知道訪問的資源是什麼,咱們再來看咱們設計的 URI:git
api/products/isExist/{userId}/{productName}
這是神馬玩意啊???這種設計徹底違背 URI 原則,首先,咱們先梳理一下,咱們想要請求的資源是什麼?沒錯,是產品(Products),但這個產品是某一個用戶下的,github
因此用戶和產品有一個上下級關係,訪問產品首先得訪問用戶,這一點要在 URI 中進行體現,其次,咱們是獲取產品?仍是判斷產品是否存在?這個概念是不一樣的,面試
產品的惟一標識和用戶同樣,都是 id,在 URI 的通常設計中,若是要訪問某一惟一標識下的資源(好比 id 爲 1 的 product),會這樣進行設計:json
api/products/{id}
HttpClient 請求中會用 HttpGet 方法(api/products/1),這樣咱們就能夠得到一個 id 爲 1 的 product,但如今的場景是,獲取產品不經過惟一標識,而是經過產品名稱,難道咱們要這樣設計:api
api/products/{productName}
咋看之下,這樣好像設計也沒毛病啊,但總以爲有些不對勁,好比若是再加一個產品大小,難道要改爲這樣:api/products/{productName}/{productSize},這種設計徹底是不恰當的,上面說到,ruby
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 請求中,就不是很恰當了。
上面只是介紹了簡單的設計場景,迴歸正題,繼續:
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:建立 |
DELETE https://www.googleapis.com/bigquery/v2/projects/datasets/?key={YOUR_API_KEY}
在我看來,沒有絕對的好與很差。若是使用第一種方法,那麼只要保證Uri的語義清晰,其實和使用第二種方法沒有太大的區別。
Uri
格式:Uri
在REST中標識了一個資源,可是在具體的API設計中,每每不能作到徹底的對於資源的映射,本文中的設計將參考比較流行的Uri
設計,大體有這麼幾條:
Uri
的根(root
, /
)應當可以標識這是一個RESTful API,以與同目錄下其餘可能存在的資源進行區分。Uri
的根,應當標識當前API的版本號。這裏仍是拿行業標杆Google的開放API來舉例:
POST
https://www.googleapis.com/books/v1/mylibrary/annotations
PUT
https://www.googleapis.com/bigquery/v2/projects/p1/datasets/p2
DELETE
https://www.googleapis.com/bigquery/v2/projects/{project-parameter}/datasets/{datasets-parameter}
REST
的大部分實現都是一個基於HTTP
的,那麼天然而然就少不了與返回碼打交道,然而不幸的是,HTTP
的返回碼定義的看起來十分隨意,不少錯誤信息語意不詳,並且在實際的開發中,
API的使用者須要處理鏈路的問題(如超時等)、種類繁多的HTTP
返回碼、和實際的返回內容,不堪其繁瑣。更嚴重的是,這些返回碼大多最終依賴於服務端開發者的具體實現,
而這種看似約定的東西分別在客戶端和服務端開發者眼中的含義可能相去甚遠。
那麼從需求入手,咱們在使用RESTful API
時須要使用返回碼的緣由大體是這樣的:客戶端在調用一個API以後,須要在接收到的反饋必需要可以標識此次調用是否成功,
若是不成功,客戶端須要拿到失敗的緣由。咱們能夠在API設計時做一個小小的約定,就能完美的知足以上需求了。
服務端在成功接收到客戶端的請求以後,永遠返回200,具體成功與否及進一步的信息放入返回的內容。
在這個場景中,若是是鏈路出了問題或者服務器錯誤等(返回碼不等於200
),客戶端很容易就能捕獲這個錯誤,若是鏈路沒問題,那麼出錯與否在獲取到的反饋內容中會有詳細的描述。
如今愈來愈多的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的Uri爲基於HTTP的名詞性短語,用來表明一種資源。
Uri格式如文中所述。
使用GET POST PUT DELETE四種方法分別表明對資源的「查詢、添加、更新、刪除」。
服務端接收到客戶端的請求以後,統一返回200,若是客戶端獲取到的返回碼不是200,表明鏈路上某一個環節出了問題。
服務端全部的響應格式爲:
{ 「code」: -23400, 「message」: 「Invalid Request」, 「data」:{ } }
他們的含義分別表明:
還沒完。。。。。這可能寫的又臭又長...可是下面是迴歸重點額,不管在面試仍是處於本身開發項目中,restful api的設計規範仍是頗有必要知曉滴。繼續個人廢話:
不該該使用動詞:
/getAllResources/createNewResources/deleteAllResources
若是要改變資源的狀態,使用PUT、POST、DELETE。下面是錯誤的用GET方法來修改user的狀態:
GET /users/211?activate GET /users/211/activate
咱們定義資源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使用者更加容易理解,對開發者來講也更容易實現。
處理關聯:
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
/
在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結果的前十個),還應該能夠進行縱向限制,而且這個功能能有效的提升網絡帶寬使用率和速度。可使用fields查詢參數來限制返回的域例如:
GET /tickets?fields=id,subject,customer_name,updated_at&state=open&sort=-updated_at
response 的 body直接就是數據,不要作多餘的包裝。錯誤實例:
{ "success":true, "data":{"id":1, "name":"周伯通"} }
在POST操做之後,返回201created 狀態碼,而且包含一個指向新資源的url做爲返回頭。
是蛇形命名仍是駝峯命名?若是使用json那麼最好的應該是遵照JavaScript的命名方法-駝峯命名法。Java、C# 使用駝峯,python、ruby使用蛇形。
開啓pretty print返回結果會更加友好易讀,並且額外的傳輸也能夠忽略不計。若是忘了使用gzip那麼傳輸效率將會大大減小,損失大大增長。
經過Accept字段來區分app版本號,而不是在url中嵌入版本號(好比迭代的v1,v2,v3等):
Accept: application/vnd.github.v3+json
Summary Representation
當你請求獲取某一資源的列表時,響應僅返回資源的屬性子集。有些屬性對API來講代價是很是高的,出於性能的考慮,會排除這些屬性。要獲取這些屬性,請求"detailed" representation。
Example:當你獲取倉庫的列表時,你得到的是每一個倉庫的summary representation。
GET /orgs/octokit/repos
Detailed Representation(詳細描述)
當你獲取一個單獨的資源時,響應會返回這個資源的全部屬性。
Example:當你獲取一個單獨的倉庫,你會得到這個倉庫的detailed representation。
GET /repos/octokit/octokit.rb
許多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'。
你能夠對根節點GET請求,獲取根節點下的全部API分類。
有三種可能的客戶端錯誤,在接收到請求體時:
1 發送非法JSON會返回 400 Bad Request.
HTTP/1.1 400 Bad Request Content-Length: 35 {"message":"Problems parsing JSON"}
2 發送錯誤類型的JSON值會返回 400 Bad Request.
HTTP/1.1 400 Bad Request Content-Length: 40 {"message":"Body should be a JSON object"}
3 發送無效的值會返回 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 | 另外一個資源的域的值和此處的相同,這會發生在資源有惟一的鍵的時候 |
API v3在合適的地方使用HTTP重定向。客戶端應該假設任何請求都會致使重定向。重定向在響應頭中有一個 Location
的域,此域包含了資源的真實位置。
API v3力爭使用正確的HTTP動詞來表示每次請求。
Verb | Description |
---|---|
HEAD | 對任何資源僅請求頭信息 |
GET | 獲取資源 |
POST | 建立資源 |
PATCH | 使用部分的JSON數據更新資源 |
PUT | 取代資源或資源集合 |
DELETE | 刪除資源 |
不少資源有一個或者更多的 *_url
屬性指向其餘資源。這意味着服務端提供明確的URL,這樣客戶端就沒必要要本身構造URL了。
請求資源列表時會進行分頁,默認每頁30個。當你請求後續頁的時候可使用 ?page
參數。對於某些資源,你能夠經過參數 ?per_page
自定義每頁的大小。
curl 'https://api.github.com/user/repos?page=2&per_page=100'
須要注意的一點是,頁碼是從1開始的,當省略參數 ?page
時,會返回首頁。
關於分頁的其餘相關信息在響應的頭信息的 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。
既然已經知道會接收多少頁面,咱們能夠經過頁面導航來消費結果。咱們能夠經過傳遞一個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之間進行跳轉。
對於認證的請求,能夠每小時最多請求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" }
全部的API請求必須包含一個有效的 User-Agent
頭。請求頭不包含User-Agent
的請求會被拒絕。
大多數響應都會返回一個 ETag
頭。不少響應也會返回一個 Last-Modified
頭。你可使用這些頭信息對這些資源進行後續請求,分別使用 If-None-Match
和 If-Modified-Since
頭。若是資源沒有發生改變,服務器端會返回 304 Not Modified
。
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-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
所規定的時間過去。
你能夠本身限制響應返回的域。只須要你傳遞一個 fields
參數,用逗號分隔所須要的域,好比:
GET /api/v1/users?fields=id,first_name
全部返回一個集合的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表示全部現存結果的數量,而不是這次響應返回的結果的數量。
若是你的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,感興趣的朋友能夠體驗哈。