1. 程式人生 > >REST介面設計規範

REST介面設計規範

原文出處

URI格式規範

  • URI(Uniform Resource Identifiers) 統一資源標示符
  • URL(Uniform Resource Locator) 統一資源定位符
URI的格式定義如下:
URI = scheme "://" authority "/" path [ "?" query ] [ "#" fragment ]

URL是URI的一個子集(一種具體實現),對於REST API來說一個資源一般對應一個唯一的URI(URL)。在URI的設計中,我們會遵循一些規則,使介面看起透明易讀,方便使用者呼叫。

  • 關於分隔符“/”的使用
"/"分隔符一般用來對資源層級的劃分,例如 http://api.canvas.restapi.org/shapes/polygons/quadrilaterals/squares

對於REST API來說,"/"只是一個分隔符,並無其他含義。為了避免混淆,"/"不應該出現在URL的末尾。例如以下兩個地址實際表示的都是同一個資源:
http://api.canvas.restapi.org/shapes/
http://api.canvas.restapi.org/shapes

REST API對URI資源的定義具有唯一性,一個資源對應一個唯一的地址。為了使介面保持清晰乾淨,如果訪問到末尾包含 "/" 的地址,服務端應該301到沒有 "/"的地址上。當然這個規則也僅限於REST API介面的訪問,對於傳統的WEB頁面服務來說,並不一定適用這個規則。
  • URI中儘量使用連字元"-"代替下劃線"_"的使用
連字元"-"一般用來分割URI中出現的字串(單詞),來提高URI的可讀性,例如:  
http://api.example.restapi.org/blogs/mark-masse/entries/this-is-my-first-post  

使用下劃線"_"來分割字串(單詞)可能會和連結的樣式衝突重疊,而影響閱讀性。但實際上,"-"和"_"對URL中字串的分割語意上還是有些差異的:"-"分割的字串(單詞)一般各自都具有獨立的含義,可參見上面的例子。而"_"一般用於對一個整體含義的字串做了層級的分割,方便閱讀,例如你想在URL中體現一個ip地址的資訊:210_110_25_88 .
  • URI中統一使用小寫字母
根據RFC3986定義,URI是對大小寫敏感的,所以為了避免歧義,我們儘量用小寫字元。但主機名(Host)和scheme(協議名稱:http/ftp/...)對大小寫是不敏感的。
  • URI中不要包含檔案(指令碼)的副檔名
例如 .php .json 之內的就不要出現了,對於介面來說沒有任何實際的意義。如果是想對返回的資料內容格式標示的話,通過HTTP Header中的Content-Type欄位更好一些。

資源的原型

  • 文件(Document)
文件是資源的單一表現形式,可以理解為一個物件,或者資料庫中的一條記錄。在請求文件時,要麼返回文件對應的資料,要麼會返回一個指向另外一個資源(文件)的連結。以下是幾個基於文件定義的URI例子:
http://api.soccer.restapi.org/leagues/seattle http://api.soccer.restapi.org/leagues/seattle/teams/trebuchet http://api.soccer.restapi.org/leagues/seattle/teams/trebuchet/players/mike
  • 集合(Collection)
集合可以理解為是資源的一個容器(目錄),我們可以向裡面新增資源(文件)。例如:
http://api.soccer.restapi.org/leagues http://api.soccer.restapi.org/leagues/seattle/teams http://api.soccer.restapi.org/leagues/seattle/teams/trebuchet/players
  • 倉庫(Store)
倉庫是客戶端來管理的一個資源庫,客戶端可以向倉庫中新增資源或者刪除資源。客戶端也可以批量獲取到某個倉庫下的所有資源。倉庫中的資源對外的訪問不會提供單獨URI的,客戶端在建立資源時候的URI除外。例如:
PUT /users/1234/favorites/alonso
上面的例子我們可以理解為,我們向一個id是1234的使用者的倉庫(收藏夾)中,添加了一個名為alonso的資源。通俗點兒說:就是使用者收藏了一個自己喜愛的球員阿隆索。
  • 控制器(Controller)
控制器資源模型,可以執行一個方法,支援引數輸入,結果返回。 是為了除了標準操作:增刪改查(CRUD)以外的一些邏輯操作。控制器(方法)一般定義子URI中末尾,並且不會有子資源(控制器)。例如我們向用戶重發ID為245743的訊息:
POST /alerts/245743/resend

URI命名規範

  • 文件(Document)型別的資源用名詞(短語)單數命名
  • 集合(Collection)型別的資源用名詞(短語)複數命名
  • 倉庫(Store)型別的資源用名詞(短語)複數命名
  • 控制器(Controller)型別的資源用**動詞(短語)**命名
  • URI中有些欄位可以是變數,在實際使用中可以按需替換
例如一個資源URI可以這樣定義:
http://api.soccer.restapi.org/leagues/{leagueId}/teams/{teamId}/players/{playerId}
其中:leagueId,teamId,playerId 是變數(數字,字串都型別都可以)。
  • CRUD的操作不要體現在URI中,HTTP協議中的操作符已經對CRUD做了對映。
CRUD是建立,讀取,更新,刪除這四個經典操作的簡稱
例如刪除的操作用REST規範執行的話,應該是這個樣子:
DELETE /users/1234

以下是幾個錯誤的示例:
GET /deleteUser?id=1234
GET /deleteUser/1234
DELETE /deleteUser/1234
POST /users/1234/delete

URI的query欄位

http://api.college.restapi.org/students/morgan/send-sms http://api.college.restapi.org/students/morgan/send-sms?text=hello

以上的兩個URI看起來很像,但實際的含義是有差別的。第一個URI是一個傳送訊息的Controller型別的API,第二個URI是傳送一個text的內容是hello的訊息。

在REST中,query欄位一般作為查詢的引數補充,也可以幫助標示一個唯一的資源。但需要注意的是,作為一個提供查詢功能的URI,無論是否有query條件,我們都應該保證結果的唯一性,一個URI對應的返回資料是不應該被改變的(在資源沒有修改的情況下)。HTTP中的快取也可能快取查詢結果,這個也是我們需要知道的。

  • Query引數可以作為Collection或Store型別資源的過濾條件來使用
    例如:
GET /users //返回所有使用者列表
GET /users?role=admin //返回許可權為admin的使用者列表
  • Query引數可以作為Collection或Store資源列表分頁標示使用
如果是一個簡單的列表操作,可以這樣設計: 
GET /users?pageSize=25&pageStartIndex=50
如果是一個複雜的列表或查詢操作的話,我們可以為資源設計一個Collection,因為複雜查詢可能會涉及比較多的引數,建議使用Post的方式傳入,例如這樣:
POST /users/search

HTTP互動設計

HTTP請求方法的使用

  • GET方法用來獲取資源
  • PUT方法可用來新增/更新Store型別的資源
  • PUT方法可用來更新一個資源
  • POST方法可用來建立一個資源
  • POST方法可用來觸發執行一個Controller型別資源
  • DELETE方法用於刪除資源
一旦資源被刪除,GET/HEAD方法訪問被刪除的資源時,要返回404
DELETE是一個比較純粹的方法,我們不能對其做任何的重構或者定義,不可附加其它狀態條件,如果我們希望"軟"刪除一個資源,則這種需求應該由Controller類資源來實現。

HTTP響應狀態碼的使用

  • 200 (“OK”) 用於一般性的成功返回
  • 200 (“OK”) 不可用於請求錯誤返回
  • 201 (“Created”) 資源被建立
  • 202 (“Accepted”) 用於Controller控制類資源非同步處理的返回,僅表示請求已經收到。對於耗時比較久的處理,一般用非同步處理來完成
  • 204 (“No Content”) 此狀態可能會出現在PUT、POST、DELETE的請求中,一般表示資源存在,但訊息體中不會返回任何資源相關的狀態或資訊。
  • 301 (“Moved Permanently”) 資源的URI被轉移,需要使用新的URI訪問
  • 302 (“Found”) 不推薦使用,此程式碼在HTTP1.1協議中被303/307替代。我們目前對302的使用和最初HTTP1.0定義的語意是有出入的,應該只有在GET/HEAD方法下,客戶端才能根據Location執行自動跳轉,而我們目前的客戶端基本上是不會判斷原請求方法的,無條件的執行臨時重定向
  • 303 (“See Other”) 返回一個資源地址URI的引用,但不強制要求客戶端獲取該地址的狀態(訪問該地址)
  • 304 (“Not Modified”) 有一些類似於204狀態,伺服器端的資源與客戶端最近訪問的資源版本一致,並無修改,不返回資源訊息體。可以用來降低服務端的壓力
  • 307 (“Temporary Redirect”) 目前URI不能提供當前請求的服務,臨時性重定向到另外一個URI。在HTTP1.1中307是用來替代早期HTTP1.0中使用不當的302
  • 400 (“Bad Request”) 用於客戶端一般性錯誤返回, 在其它4xx錯誤以外的錯誤,也可以使用400,具體錯誤資訊可以放在body中
  • 401 (“Unauthorized”) 在訪問一個需要驗證的資源時,驗證錯誤
  • 403 (“Forbidden”) 一般用於非驗證性資源訪問被禁止,例如對於某些客戶端只開放部分API的訪問許可權,而另外一些API可能無法訪問時,可以給予403狀態
  • 404 (“Not Found”) 找不到URI對應的資源
  • 405 (“Method Not Allowed”) HTTP的方法不支援,例如某些只讀資源,可能不支援POST/DELETE。但405的響應header中必須宣告該URI所支援的方法
  • 406 (“Not Acceptable”) 客戶端所請求的資源資料格式型別不被支援,例如客戶端請求資料格式為application/xml,但伺服器端只支援application/json
  • 409 (“Conflict”) 資源狀態衝突,例如客戶端嘗試刪除一個非空的Store資源
  • 412 (“Precondition Failed”) 用於有條件的操作不被滿足時
  • 415 (“Unsupported Media Type”) 客戶所支援的資料型別,服務端無法滿足
  • 500 (“Internal Server Error”) 伺服器端的介面錯誤,此錯誤於客戶端無關

原資料設計

HTTP Headers

  • Content-Type 標示body的資料格式
  • Content-Length body 資料體的大小,客戶端可以根據此標示檢驗讀取到的資料是否完整,也可以通過Header判斷是否需要下載可能較大的資料體
  • Last-Modified 用於伺服器端的響應,是一個資源最後被修改的時間戳,客戶端(快取)可以根據此資訊判斷是否需要重新獲取該資源
  • ETag 伺服器端資源版本的標示,客戶端(快取)可以根據此資訊判斷是否需要重新獲取該資源,需要注意的是,ETag如果通過伺服器隨機生成,可能會存在多個主機對同一個資源產生不同ETag的問題
  • Store型別的資源要支援有條件的PUT請求
假設有兩個客戶端client#1/#2都向一個Store資源提交PUT請求,服務端是無法清楚的判斷是要insert還是要update的,所以我們要在header中加入條件標示if-Match,If-Unmodified-Since 來明確是本次呼叫API的意圖。例如:

client#1第一次向服務端發起一個請求 PUT /objects/2113 此時2113資源還不存在,那服務端會認為本次請求是一個insert操作,完成後,會返回 201 (“Created”)
  
client#2再一次向服務端發起同一個請求 PUT /objects/2113 時,因2113資源已存在,服務端會返回 409 (“Conflict”)

為了能讓client#2的請求成功,或者說我們要清楚的表明本次操作是一次update操作,我們必須在header中加入一些條件標示,例如 if-Match。我們需要給出資源的ETag(if-Match:Etag),來表明我們希望更新資源的版本,如果服務端版本一致,會返回200 (“OK”) 或者 204 (“No Content”)。如果服務端發現指定的版本與當前資源版本不一致,會返回 412 (“Precondition Failed”)
  • Location 在響應header中使用,一般為客戶端感興趣的資源URI,例如在成功建立一個資源後,我們可以把新的資源URI放在Location中,如果是一個非同步建立資源的請求,介面在響應202 (“Accepted”)的同時可以給予客戶端一個非同步狀態查詢的地址

  • Cache-Control, Expires, Date 通過快取機制提升介面響應效能,同時根據實際需要也可以禁止客戶端對介面請求做快取。對於REST介面來說,如果某些介面實時性要求不高的情況下,我們可以使用max-age來指定一個小的快取時間,這樣對客戶端和伺服器端雙方都是有利的。一般來說只對GET方法且返回200的情況下使用快取,在某些情況下我們也可以對返回3xx或者4xx的情況下做快取,可以防範錯誤訪問帶來的負載。

  • 我們可以自定義一些頭資訊,作為客戶端和伺服器間的通訊使用,但不能改變HTTP方法的性質。自定義頭儘量簡單明瞭,不要用body中的資訊對其作補充說明。

資料媒體型別(Media Type)

定義如下:

Content-Type: type "/" subtype *( ";" parameter )
兩個例項:
Content-type: text/html; charset=ISO-8859-4
Content-type: text/plain; charset="us-ascii"

type 主型別一般為:application, audio, image, message, model, multipart, text, video。REST介面的主型別一般使用application

資料媒體型別(Media Type)設計

  • 設計上來說,伺服器端可以支援多種媒體型別
  • 可以通過URI的查詢欄位來指定客戶端希望的資料型別
GET /bookmarks/mikemassedotcom?accept=application/xml

資料媒體格式的設計

body的媒體格式

  • json是一種流行且輕便友好的格式,json是一種無序的鍵值對的集合,其中key是需要用雙引號引起來的,value如果是數字可以不用雙引號,如果是非數字的格式需要使用雙引號。
這是一個json格式的例子:
{
"firstName" : "Osvaldo",
"lastName" : "Alonso", "firstNamePronunciation" : "ahs-VAHL-doe", "number" : 6,
"birthDate" : "1985-11-11"
}
  • json是允許大小寫混用命名的,但要避免使用特殊符號
  • 除了json我們也可以使用其他常用的格式,例如xml,html等
  • body本身只應包含資源相關的資訊,不要附加其它傳輸狀態的資訊

錯誤響應描述

  • 錯誤資訊的格式應該保持一致,例如用以下方式(json格式):
{
  "id" : Text,  //錯誤唯一標示id
  "description" : Text  //錯誤具體描述
}

如果有多個錯誤,可以用json陣列來描述:
{
  "elements" : [
    {
      "id" : "Update Failed",
      "description" : "Failed to update /users/1234"
    }
  ]
}
  • 錯誤型別需要保持統一

客戶端關注的問題

介面版本管理

  • 一個資源,只用一種單一的URI來標示,資源的版本不應該體現在URI中
  • 資源的版本是可以由客戶端來指定的,並且提供向後相容
  • ETag可以用來管理資源的版本,有助於客戶端快取的應用

介面的安全

  • 使用OAuth認證,對敏感資源保護
  • 使用API管理策略,或管理平臺(Apigee, Mashery)

介面資料響應的結構

  • 客戶端可以指定介面返回需要的資源欄位,或者指定不希望返回的欄位,這樣有助於提升介面互動的效率,較少頻寬的浪費
只獲許部分欄位:
GET /students/morgan?fields=(firstName, birthDate)

不希望獲取某些欄位:
GET /students/morgan?fields=!(address,schedule!(wednesday, friday))

  • 資源資料中可以包含嵌入式連結,用來描述查詢資源的子集,我們也可以傳入相關引數,要求服務端替換連結為實際的資料
{
  "firstName" : "Morgan", 
  "birthDate" : "1992-07-31",
  # Other fields...
  "links" : {
    "self" : {
      "href" : "http://api.college.restapi.org/students/morgan",
      "rel" : "http://api.relations.wrml.org/common/self" 
    },
    "favoriteClass" : {
      "href" : "http://api.college.restapi.org/classes/japn-301",    
      "rel" : "http://api.relations.wrml.org/college/favoriteClass"
    },
    # Other links... 
  }
}

如果我們傳入embed=(favoriteClass)的引數,返回的資料中將用實際的內容替換links裡的對應的潛入資源:
# Request
GET /students/morgan?embed=(favoriteClass)

# Response
{
  "firstName" : "Morgan",
  "birthDate" : "1992-07-31", 
  "favoriteClass" : { //需要返回的嵌入資料
    "id" : "japn-301",
    "name" : "Third-Year Japanese", 
    "links" : {
      "self" : {
        "href" : "http://api.college.restapi.org/classes/japn-301",     
        "rel" : "http://api.relations.wrml.org/common/self"
      } 
    }
}

# Other fields...
  "links" : { 
    "self" : {
      "href" : "http://api.college.restapi.org/students/morgan",
      "rel" : "http://api.relations.wrml.org/common/self" 
    },
    # 之前的嵌入式連結favoriteClass,已被替換為實體資料
    # Other links... 
  }
}


#其中嵌入式連結資訊中的 rel ,一般是對 href 資源如何互動的描述,例如是通過 GET 還是 POST 方法,可以是以下的結構:
{
  "name": "morgan",
  "method": "GET", 
  ... #其它描述欄位
}

JavaScript客戶端

目前主流的瀏覽器對JavaScript的支援越來越完善,因此對於WEB應用來說,我們完全可以把客戶單看成一個JavaScript客戶端。

  • 一般瀏覽器對於跨域的操作都有一定的安全策略,通常我們可以使用JSONP來解決跨域介面訪問的限制
  • 通過CORS(Cross-Origin Resource Sharing)來解決跨域訪問,此方法與JSONP相比,支援更多的方法,JSONP只能用於GET請求, 一般現代的瀏覽器會支援CORS的方式

本文內容參考/引用於:
Mark.Masse《REST.API.Design.Rulebook》