RESTful API 設計最佳實踐

來源:http://www.oschina.net/translate/best-practices-for-a-pragmatic-restful-api

  數據模型已經穩定,接下來你可能需要爲web(網站)應用創建一個公開的API(應用程序編程接口)。需要認識到這樣一個問題:一旦API發佈後,就很難對它做很大的改動並且保持像先前一樣的正確性。現在,網絡上有很多關於API設計的思路。但是在全部案例中沒有一種被廣泛採納的標準,有很多的選擇:你接受什麼樣的格式?如何認證?API應該被版本化嗎?

在爲SupportFu(一個輕量級的Zendesk替換實現)設計API時,對於這些問題我儘量得出一些務實的答案。我的目標是設計這樣一個API,它容易使用和採納,足夠靈活去爲我們用戶接口去埋單。

 

API的關鍵要求

  許多網上能找到的API設計觀點都是些學術討論,這些討論是關於模糊標準的主觀解釋,而不是關於在現實世界中具有意義的事。本文中我的目標是,描述一下爲當今的web應用而設計的實用的API的最佳實踐。如果感覺不對,我不會去嘗試滿足某個標準。爲了幫助進行決策,我已經寫下了API必須力爭滿足的一些要求:

  • 它應當在需要的地方使用 web 標準
  • 它應當對開發者友好並且便於在瀏覽器地址欄中瀏覽和探索
  • 它應當是簡單、直觀和一致的,使它用起來方便和舒適
  • 它應當提供足夠的靈活性來增強大多數的 SupportFu 用戶界面
  • 它應當是高效的,同時要維持和其他需求之間的平衡

  一個 API 是一個開發者的 UI - 就像其他任何 UI 一樣, 確保用戶體驗被認真的考慮過是很重要的!

 

使用 RESTful URLs and actions

  如果有一樣東西獲得廣泛認可的話,那就是 RESTful 原則。Roy Felding 在他論文 network based software architectures 的 第五章 中首次介紹了這些原則。

這些REST的關鍵原則與將你的 API 分割成邏輯資源緊密相關。使用HTTP請求控制這些資源,其中,這些方法(GET, POST, PUT, PATCH, DELETE)具有特殊含義。

可是我該整出什麼樣的資源呢?好吧,它們應該是有意義於 API 使用者的名詞(不是動詞)。雖然內部Model可以簡單地映射到資源上,但那不一定是個一對一的映射。這裏的關鍵是不要泄漏與API不相關的實現細節。一些相關的名詞可以是票,用戶和小組

 

一旦定義好了資源, 需要確定什麼樣的 actions 應用它們,這些 actions 怎麼映射到你的 API 上。RESTful 原則提供了 HTTP methods 映射作爲策略來處理 CRUD actions,如下:

  • GET /tickets - 獲取 tickets 列表
  • GET /tickets/12 - 獲取一個單獨的 ticket
  • POST /tickets - 創建一個新的 ticket
  • PUT /tickets/12 - 更新 ticket #12
  • PATCH /tickets/12 - 部分更新 ticket #12
  • DELETE /tickets/12 - 刪除 ticket #12

REST 非常棒的是,利用現有的 HTTP 方法在單個的 /tickets 接入點上實現了顯著的功能。沒有什麼方法命名約定需要去遵循,URL 結構是整潔乾淨的。 REST 太棒了!

 

接入點的名稱應該選擇單數還是複數呢?keep-it-simple原則可以在此應用。雖然你內在的語法知識會告訴你用複數形式描述單一資源實例是錯誤的,但實用主義的答案是保持URL格式一致並且始終使用複數形式。不用處理各種奇形怪狀的複數形式(比如person/people,goose/geese)可以讓API消費者的生活更加美好,也讓API提供者更容易實現API(因爲大多數現代框架天然地將/tickets和/tickets/12放在同一個控制器下處理)。

但是你該如何處理(資源的)關係呢?如果關係依託於另外一個資源,Restful原則提供了很好的指導原則。讓我們來看一個例子。SupportFu的一個ticket包含許多消息(message)。這些消息邏輯上與/tickets接入點的映射關係如下:

  • GET /tickets/12/messages - 獲取ticket #12下的消息列表
  • GET /tickets/12/messages/5 - 獲取ticket #12下的編號爲5的消息
  • POST /tickets/12/messages - 爲ticket #12創建一個新消息
  • PUT /tickets/12/messages/5 - 更新ticket #12下的編號爲5的消息
  • PATCH /tickets/12/messages/5 - 部分更新ticket #12下的編號爲5的消息
  • DELETE /tickets/12/messages/5 - 刪除ticket #12下的編號爲5的消息

 

或者如果某種關係不依賴於資源,那麼在資源的輸出表示中只包含一個標識符是有意義的。API消費者然後除了請求資源所在的接入點外,還得再請求一次關係所在的接入點。但是如果一般情況關係和資源一起被請求,API可以提供自動嵌套關係表示到資源表示中,這樣可以防止兩次請求API。

如果Action不符合CRUD操作那該怎麼辦?

這是一個可能讓人感到模糊不解的地方。有幾種處理方法:

  1. 重新構造這個Action,使得它像一個資源的field(我理解爲部分域或者部分字段)。這種方法在Action不包含參數的情況下可以奏效。例如一個有效的action可以映射成布爾類型field,並且可以通過PATCH更新資源。
  2. 利用RESTful原則像處理子資源一樣處理它。例如,Github的API讓你通過PUT /gists/:id/star 來 star a gist ,而通過DELETE /gists/:id/star來進行 unstar 。
  3. 有時候你實在是沒有辦法將Action映射到任何有意義的RESTful結構。例如,多資源搜索沒辦法真正地映射到任何一個資源接入點。這種情況,/search 將非常有意義,雖然它不是一個名詞。這樣做沒有問題 - 你只需要從API消費者的角度做正確的事,並確保所做的一切都用文檔清晰記錄下來了以避免(API消費者的)困惑。

 

總是使用 SSH

總是使用SSL,沒有例外。今天,您的web api可以從任何地方訪問互聯網(如圖書館、咖啡店、機場等)。不是所有這些都是安全的,許多不加密通信,便於竊聽或僞造,如果身份驗證憑證被劫持。

另一個優點是,保證總是使用SSL加密通信簡化了認證效果——你可以擺脫簡單的訪問令牌,而不是讓每個API請求籤署。

要注意的一點是非SSL訪問API URLs。不要重定向這些到對應的SSL。相反,拋出一個系統錯誤!最後一件你想要的是配置不佳的客戶發送請求到一個未加密的端點,只是默默地重定向到實際加密的端點

 

文檔

API的好壞關鍵看其文檔的好壞. 好的API的說明文檔應該很容易就被找到,並能公開訪問。在嘗試任何整合工作前大部分開發者會先查看其文檔。當文檔被藏於一個PDF之中或要求必須登記信息時,將很難被找到也很難搜索到。

好的文檔須提供從請求到響應整個循環的示例。最好的是,請求應該是可粘貼的例子,要麼是可以貼到瀏覽器的鏈接,要麼是可以貼到終端裏的curl示例 。 GitHub 和 Stripe 在這方面做的非常出色。

一旦你發佈一個公開的API,你必須承諾"在沒有通告的前提下,不會更改APIDe功能" .對於外部可見API的更新,文檔必須包含任何將廢棄的API的時間表和詳情。應該通過博客(更新日誌)或者郵件列表送達更新說明(最好兩者都通知)。 

 

版本控制

必須對API進行版本控制。版本控制可以快速迭代並避免無效的請求訪問已更新的接入點。它也有助於幫助平滑過渡任何大範圍的API版本變遷,這樣就可以繼續支持舊版本API。

關於API的版本是否應該包含在URL或者請求頭中 莫衷一是。從學術派的角度來講,它應該出現在請求頭中。然而版本信息出現在URL中必須保證不同版本資源的瀏覽器可瀏覽性(browser explorability),還記得文章開始提到的API要求嗎?

我非常贊成 approach that Stripe has taken to API versioning - URL包含一個主版本號(比如http://shonzilla/api/v1/customers/1234)
),但是API還包含基於日期的子版本(比如http://shonzilla/api/v1.2/customers/1234),可以通過配置HTTP請求頭來進行選擇。這種情況下,主版本確保API結構總體穩定性,而子版本會考慮細微的變化(field deprecation、接入點變化等)。

API不可能完全穩定。變更不可避免,重要的是變更是如何被控制的。維護良好的文檔、公佈未來數月的deprecation計劃,這些對於很多API來說都是一些可行的舉措。它歸根結底是看對於業界和API的潛在消費者是否合理。

 

結果過濾,排序和搜索

最好是儘量保持基本資源URL的簡潔性。 複雜結果過濾器、排序需求和高級搜索 (當限定在單一類型的資源時) ,都能夠作爲在基本URL之上的查詢參數來輕鬆實現。下面讓我們更詳細的看一下:

過濾: 對每一個字段使用一個唯一查詢參數,就可以實現過濾。 例如,當通過“/tickets”終端來請求一個票據列表時,你可能想要限定只要那些在售的票。這可以通過一個像 GET /tickets?state=open 這樣的請求來實現。這裏“state”是一個實現了過濾功能的查詢參數。

排序: 跟過濾類似, 一個泛型參數排序可以被用來描述排序的規則. 爲適應複雜排序需求,讓排序參數採取逗號分隔的字段列表的形式,每一個字段前都可能有一個負號來表示按降序排序。我們看幾個例子:

  • GET /tickets?sort=-priority - 獲取票據列表,按優先級字段降序排序
  • GET /tickets?sort=-priority,created_at - 獲取票據列表,按“priority”字段降序排序。在一個特定的優先級內,較早的票排在前面。

 

搜索: 有時基本的過濾不能滿足需求,這時你就需要全文檢索的力量。或許你已經在使用  ElasticSearch 或者其它基於 Lucene 的搜索技術。當全文檢索被用作獲取某種特定資源的資源實例的機制時, 它可以被暴露在API中,作爲資源終端的查詢參數,我們叫它“q”。搜索類查詢應當被直接交給搜索引擎,並且API的產出物應當具有同樣的格式,以一個普通列表作爲結果。

把這些組合在一起,我們可以創建以下一些查詢:

  • GET /tickets?sort=-updated_at - 獲取最近更新的票
  • GET /tickets?state=closed&sort=-updated_at - 獲取最近更新並且狀態爲關閉的票。
  • GET /tickets?q=return&state=open&sort=-priority,created_at - 獲取優先級最高、最先創建的、狀態爲開放的票,並且票上有 'return' 字樣。

一般查詢的別名

爲了使普通用戶的API使用體驗更加愉快, 考慮把條件集合包裝進容易訪問的RESTful 路徑中。比如上面的,最近關閉的票的查詢可以被包裝成 GET /tickets/recently_closed

 

限制哪些字段由API返回

API的使用者並不總是需要一個資源的完整表示。選擇返回字段的功能由來已久,它使得API使用者能夠最小化網絡阻塞,並加速他們對API的調用。

使用一個字段查詢參數,它包含一個用逗號隔開的字段列表。例如,下列請求獲得的信息將剛剛足夠展示一個在售票的有序列表:

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

 

更新和創建應該返回一個資源描述

一個 PUT, POST 或者 PATCH 調用可能會對指定資源的某些字段造成更改,而這些字段本不在提供的參數之列 (例如: created_at 或 updated_at 這兩個時間戳)。 爲了防止API使用者爲了獲取更新後的資源而再次調用該API,應當使API把更新(或創建)後的資源作爲response的一部分來返回。

以一個產生創建活動的 POST 操作爲例, 使用一個 HTTP 201 狀態代碼 然後包含一個 Location header 來指向新生資源的URL。

 

你是否應該HATEOAS?

(譯註:Hypermedia as the Engine of Application State (HATEOAS)超媒體作爲應用程序狀態引擎)

對於API消費方是否應該創建鏈接,或者是否應該將鏈接提供給API,有許多混雜的觀點。RESTful的設計原則指定了HATEOAS ,大致說明了與某個端點的交互應該定義在元數據(metadata)之中,這個元數據與輸出結果一同到達,並不基於其他地方的信息。

雖然web逐漸依照HATEOAS類型的原則運作(我們打開一個網站首頁並隨着我們看到的頁面中的鏈接瀏覽),我不認爲我們已經準備好API的HATEOAS了。當瀏覽一個網站的時候,決定點擊哪個鏈接是運行時做出的。然而,對於API,決定哪個請求被髮送是在寫API集成代碼時做出的,並不是運行時。這個決定可以移交到運行時嗎?當然可以,不過順着這條路沒有太多好處,因爲代碼仍然不能不中斷的處理重大的API變化。也就是說,我認爲HATEOAS做出了承諾,但是還沒有準備好迎接它的黃金時間。爲了完全實現它的潛能,需要付出更多的努力去定義圍繞着這些原則的標準和工具。

目前而言,最好假定用戶已經訪問過輸出結果中的文檔&包含資源標識符,而這些API消費方會在製作鏈接的時候用到。關注標識符有幾個優勢——網絡中的數據流減少了,API消費方存儲的數據也減少了(因爲它們存儲的是小的標識符而不是包含標識符的URLs)。

同樣的,在URL中提供本文倡導的版本號,對於在一個很長時間內API消費方存儲資源標識符(而不是URLs),它更有意義。總之,標識符相對版本是穩定的,但是表示這一點的URL卻不是的!

只返回JSON

是時候在API中丟棄XML了。XML冗長,難以解析,很難讀,他的數據模型和大部分編程語言的數據模型 不兼容,而他的可擴展性優勢在你的主要需求是必須序列化一個內部數據進行輸出展示時變得不相干。

我不打算對上述進行解釋了,貌似諸如 (YouTubeTwitter 和 Box)之類的已經開始了去XML化.

給你一張google趨勢圖,比較XML API 和 JSON API的,供你參考:

但是,如果你的客戶羣包括大量的企業客戶,你會發現自己不得不支持XML的方式。如果你必須這樣,一個新問題出現了:

媒體類型是應該基於Accept頭還是基於URL呢 ? 爲確保瀏覽器的瀏覽性,應該基於URL。這裏最明智的選擇是在端點URL後面附加 .json 或 .xml 的擴展.

 

字段名稱書寫格式的 snake_case vs camelCase

如果你在使用JSON (JavaScript Object Notation) 作爲你的主要表示格式,正確的方法就是遵守JavaScript命名約定——對字段名稱使用camelCase!如果你要走用各種語言建設客戶端庫的路線,最好使用它們慣用的命名約定—— C# & Java 使用camelCase, python & ruby 使用snake_case。

深思:我一直認爲snake_case比JavaScript的camelCase約定更容易閱讀。我沒有任何證據來支持我的直覺,直到現在,基於從2010年的camelCase 和 snake_case的眼動追蹤研究 (PDF),snake_case比駝峯更容易閱讀20%!這種閱讀上的影響會影響API的可勘探性和文檔中的示例。

許多流行的JSON API使用snake_case。我懷疑這是由於序列化庫遵從它們所使用的底層語言的命名約定。也許我們需要有JSON序列庫來處理命名約定轉換。

 

缺省情況下確保漂亮的打印和支持gzip

一個提供空白符壓縮輸出的API,從瀏覽器中查看結果並不美觀。雖然一些有序的查詢參數(如 ?pretty=true )可以提供來使漂亮打印生效,一個默認情況下能進行漂亮打印的API更爲平易近人。額外數據傳輸的成本是微不足道的,尤其是當你比較不執行gzip壓縮的成本。

考慮一些用例:假設分析一個API消費者正在調試並且有自己的代碼來打印出從API收到的數據——默認情況下這應是可讀的。或者,如果消費者抓住他們的代碼生成的URL,並直接從瀏覽器訪問它——默認情況下這應是可讀的。這些都是小事情。做好小事情會使一個API能被更愉快地使用!

 

 

那麼該如何處理額外傳輸的數據呢?

讓我們看一個實際例子。我從GitHub API上拉取了一些數據,默認這些數據使用了漂亮打印(pretty print)。我也將做一些GZIP壓縮後的對比。

 
$ curl https://api.github.com/users/veesahni > with-whitespace.txt
$ ruby -r json -e 'puts JSON JSON.parse(STDIN.read)' < with-whitespace.txt > without-whitespace.txt
$ gzip -c with-whitespace.txt > with-whitespace.txt.gz
$ gzip -c without-whitespace.txt ? without-whitespace.txt.gz

輸出文件的大小如下:

  • without-whitespace.txt - 1252 bytes
  • with-whitespace.txt - 1369 bytes
  • without-whitespace.txt.gz - 496 bytes
  • with-whitespace.txt.gz - 509 bytes

在這個例子中,當未啓用GZIP壓縮時空格增加了8.5%的額外輸出大小,而當啓用GZIP壓縮時這個比例是2.6%。另一方面,GZIP壓縮節省了60%的帶寬。由於漂亮打印的代價相對比較小,最好默認使用漂亮打印,並確保GZIP壓縮被支持。

關於這點想了解更多的話,Twitter發現當對他們的 Streaming API 開啓GZIP支持後可以在某些情況獲得 80%的帶寬節省 。Stack Exchange甚至強制要求必須對API請求結果使用GZIP壓縮(never return a response that's not compressed)。

 

不要默認使用大括號封裝,但要在需要的時候支持

許多API會像下面這樣包裹他們的響應信息:

{
    "data" : {
    "id" : 123,
    "name" : "John"
     }
}

 

有不少這樣做的理由 - 更容易附加元數據或者分頁信息,一些REST客戶端不允許輕易的訪問HTTP頭信息,並且JSONP請求不能訪問HTTP頭信息。無論怎樣,隨着迅速被採用的標準,比如CORSLink header from RFC 5988, 大括號封裝開始變得不必要。

我們應當默認不使用大括號封裝,而僅在特殊情況下使用它,從而使我們的API面向未來。

 

特殊情況下該如何使用大括號封裝?

有兩種情況確實需要大括號封裝 - 當API需要通過JSONP來支持跨域的請求時,或者當客戶端沒有能力處理HTTP頭信息時。

JSONP 請求附帶有一個額外的查詢參數(通常稱爲callback或jsonp) 表示了回調函數的名稱。如果提供了這個參數,API應當切換至完整封裝模式,這時它總是用200HTTP狀態碼作爲響應,然後把真實的狀態碼放入JSON有效載荷中。任何被一併添加進響應中的額外的HTTP頭信息都應當被映射到JSON字段中, 像這樣:

複製代碼
callback_function({
    status_code: 200,
    next_page: "https://..",
    response: {
    ... actual JSON response body ... 
    }
})
複製代碼

 

類似的,爲了支持HTTP受限的客戶端,可以允許一個特殊的查詢參數“?envelope=true”來觸發完整封裝(沒有JSONP回調函數)。

 

使用JSON 編碼的 POST, PUT & PATCH 請求體

如果你正在跟隨本文中講述的開發過程,那麼你肯定已經接受JSON作爲API的輸出。下面讓我們考慮使用JSON作爲API的輸入。

許多API在他們的API請求體中使用URL編碼。URL編碼正如它們聽起來那樣 - 將使用和編碼URL查詢參數時一樣的約定,對請求體中的鍵值對進行編碼。這很簡單,被廣泛支持而且實用。

然而,有幾個問題使得URL編碼不太好用。首先,它沒有數據類型的概念。這迫使API從字符串中轉換整數和布爾值。而且,它並沒有真正的層次結構的概念。儘管有一些約定,可以用鍵值對構造出一些結構(比如給一個鍵增加“[]”來表示一個數組),但還是不能跟JSON原生的層次結構相比。

如果API很簡單,URL編碼可以滿足需要。然而,複雜API應當嚴格對待他們的JSON格式的輸入。不論哪種方式,選定一個並且整套API要保持一致。

一個能接受JSON編碼的POST, PUT 和 PATCH請求的API,應當也需要把Content-Type頭信息設置爲application/json,或者拋出一個415不支持的媒體類型(Unsupported Media Type)的HTTP狀態碼。

 

分頁

信封喜歡將分頁信息包含在信封自身的API。我不能指責這點——直到最近,我們才找到更好的方法。正確的方法是使用RFC 5988 中介紹的鏈接標頭

使用鏈接標頭的API可以返回一系列線程的鏈接,API使用者無需自行生成鏈接。這在分頁時指針導向 非常重要。下面是抓取自 Github的正確使用鏈接標頭的文件:

Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next", <https://api.github.com/user/repos?page=50&per_page=100>; rel="last"

不過這個並非完成版本,因爲很多 API 喜歡返回額外信息,例如可用結果的總數。需要發送數量的 API 可用類似 X-Total-Count 的普通 HTTP 標頭。

 

自動裝載相關的資源描述

在很多種情況下,API的使用者需要加載和被請求資源相關的數據(或被請求資源引用的數據)。與要求使用者反覆訪問API來獲取這些信息相比,允許在請求原始資源的同時一併返回和裝載相關資源,將會帶來明顯的效率提升。

然而, 由於這樣確實 有悖於一些RESTful原則, 所以我們可以只使用一個內置的(或擴展)的查詢參數來實現這一功能,來最小化與原則的背離。

這種情況下,“embed”將是一個逗號隔開的需要被內置的字段列表。點號可以用來表示子字段。例如:

GET /ticket/12?embed=customer.name,assigned_user

這將返回一個附帶有詳細內置信息的票據,如下:

複製代碼
 {
    "id" : 12,
    "subject" : "I have a question!",
    "summary" : "Hi, ....",
    "customer" : {
    "name" : "Bob"
    },
     assigned_user: {
    "id" : 42,
    "name" : "Jim",
    }
 }
複製代碼

 

當然,實現類似於這種功能的能力完全依賴於內在的複雜度。這種內置的做法很容易產生 N+1 select 問題

 

重寫/覆蓋   HTTP 方法

一些HTTP客戶端僅能處理簡單的的GET和POST請求,爲照顧這些功能有限的客戶端,API需要一種方式來重寫HTTP方法. 儘管沒有一些硬性標準來做這事,但流行的慣例是接受一種叫 X-HTTP的請求頭,重寫是用一個字符串值包含PUT,PATCH或DELETE中的一個。

注意重寫頭應當僅接受POST請求,GET請求絕不應該 更改服務器上的數據!

速率限制

爲了防止濫用,標準的做法是給API增加某種類型的速率限制。RFC 6585 中介紹了一個HTTP狀態碼429 請求過多來實現這一點。

不論怎樣,在用戶實際受到限制之前告知他們限制的存在是很有用的。這是一個現在還缺乏標準的領域,但是已經有了一些流行的使用HTTP響應頭信息的慣用方法。

最少時包含下列頭信息(使用Twitter的命名約定 來作爲頭信息,通常沒有中間詞的大寫):

  • X-Rate-Limit-Limit - 當期允許請求的次數
  • X-Rate-Limit-Remaining - 當期剩餘的請求次數
  • X-Rate-Limit-Reset - 當期剩餘的秒數

爲什麼對X-Rate-Limit-Reset不使用時間戳而使用秒數?

一個時間戳包含了各種各樣的信息,比如日期和時區,但它們卻不是必需的。一個API使用者其實只是想知道什麼時候能再次發起請求,對他們來說一個秒數用最小的額外處理回答了這個問題。同時規避了時鐘偏差的問題。

有些API給X-Rate-Limit-Reset使用UNIX時間戳(紀元以來的秒數)。不要這樣做!

爲什麼對X-Rate-Limit-Reset使用UNIX時間戳是不好的做法?

HTTP 規範已經指定使用RFC 1123 的日期格式 (目前被使用在日期If-Modified-Since & Last-Modified HTTP頭信息中)。如果我們打算指定一種使用某種形式時間戳的、新的HTTP頭信息,我們應當遵循RFC 1123規定,而不是使用UNIX時間戳。

 

認證

一個 RESTful API 應當是無狀態的。這意味着認證請求應當不依賴於cookie或session。相反,每一個請求都應當攜帶某種類型的認證憑證。

由於總是使用SSL,認證憑證能夠被簡化爲一個隨機產生的訪問令牌,裏面傳入一個使用HTTP Basic Auth的用戶名字段。這樣做的極大的好處是,它是完全的瀏覽器可探測的 - 如果瀏覽器從服務器收到一個401未授權狀態碼,它僅需要一個彈出框來索要憑證即可。

然而,這種基於基本認證的令牌的認證方法,僅在滿足下列情形時纔可用,即用戶可以把令牌從一個管理接口複製到API使用者環境。當這種情形不能成立時,應當使用OAuth 2來產生安全令牌並傳遞給第三方。OAuth 2使用了承載令牌(Bearer tokens) 並且依賴於SSL的底層傳輸加密。

 

一個需要支持JSONP的API將需要第三種認證方法,因爲JSONP請求不能發送HTTP基本認證憑據(HTTP Basic Auth)或承載令牌(Bearer tokens) 。這種情況下,可以使用一個特殊的查詢參數access_token。注意,使用查詢參數token存在着一個固有的安全問題,即大多數的web服務器都會把查詢參數記錄到服務日誌中。

這是值得的,所有上面三種方法都只是跨API邊界兩端的傳遞令牌的方式。實際的底層令牌本身可能都是相同的。

 

緩存

HTTP 提供了一套內置的緩存框架! 所有你必須做的是,包含一些額外的出站響應頭信息,並且在收到一些入站請求頭信息時做一點兒校驗工作。

有兩種方式: ETagLast-Modified

ETag: 當產生一個請求時, 包含一個HTTP 頭,ETag會在裏面置入一個和表達內容對應的哈希值或校驗值。這個值應當跟隨表達內容的變化而變化。現在,如果一個入站HTTP請求包含了一個If-None-Match頭和一個匹配的ETag值,API應當返回一個304未修改狀態碼,而不是返回請求的資源。

Last-Modified: 基本上像ETag那樣工作,不同的是它使用時間戳。在響應頭中,Last-Modified包含了一個RFC 1123格式的時間戳,它使用If-Modified-Since來進行驗證。注意,HTTP規範已經有了 3 種不同的可接受的日期格式 ,服務器應當準備好接收其中的任何一種。

 

錯誤

就像一個HTML錯誤頁面給訪問者展示了有用的錯誤信息一樣,一個API應當以一種已知的可使用的格式來提供有用的錯誤信息。 錯誤的表示形式應當和其它任何資源沒有區別,只是有一套自己的字段。

API應當總是返回有意義的HTTP狀態代碼。API錯誤通常被分成兩種類型: 代表客戶端問題的400系列狀態碼和代表服務器問題的500系列狀態碼。最簡情況下,API應當把便於使用的JSON格式作爲400系列錯誤的標準化表示。如果可能(意思是,如果負載均衡和反向代理能創建自定義的錯誤實體), 這也適用於500系列錯誤代碼。

一個JSON格式的錯誤信息體應當爲開發者提供幾樣東西 - 一個有用的錯誤信息,一個唯一的錯誤代碼 (能夠用來在文檔中查詢詳細的錯誤信息) 和可能的詳細描述。這樣一個JSON格式的輸出可能會像下面這樣:
{
    "code" : 1234,
    "message" : "Something bad happened :(",
    "description" : "More details about the error here"
}

 

對PUT, PATCH和POST請求進行錯誤驗證將需要一個字段分解。下面可能是最好的模式:使用一個固定的頂層錯誤代碼來驗證錯誤,並在額外的字段中提供詳細錯誤信息,就像這樣:

複製代碼
{
    "code" : 1024,
    "message" : "Validation Failed",
    "errors" : [
    {
    "code" : 5432,
    "field" : "first_name",
    "message" : "First name cannot have fancy characters"
    },
    {
    "code" : 5622,
     "field" : "password",
     "message" : "Password cannot be blank"
    }
    ]
}
複製代碼

 

 

 

HTTP 狀態代碼

HTTP定義了一套可以從API返回的有意義的狀態代碼。 這些代碼能夠用來幫助API使用者對不同的響應做出相應處理。我已經把你必然會用到的那些列成了一個簡短的清單:

  • 200 OK (成功) - 對一次成功的GET, PUT, PATCH 或 DELETE的響應。也能夠用於一次未產生創建活動的POST
  • 201 Created (已創建) - 對一次導致創建活動的POST的響應。 同時結合使用一個位置頭信息指向新資源的位置- Response to a POST that results in a creation. Should be combined with a Location header pointing to the location of the new resource
  • 204 No Content (沒有內容) - 對一次沒有返回主體信息(像一次DELETE請求)的請求的響應
  • 304 Not Modified (未修改) - 當使用HTTP緩存頭信息時使用304
  • 400 Bad Request (錯誤的請求) - 請求是畸形的, 比如無法解析請求體
  • 401 Unauthorized (未授權) - 當沒有提供或提供了無效認證細節時。如果從瀏覽器使用API,也可以用來觸發彈出一次認證請求
  • 403 Forbidden (禁止訪問) - 當認證成功但是認證用戶無權訪問該資源時
  • 404 Not Found (未找到) - 當一個不存在的資源被請求時
  • 405 Method Not Allowed (方法被禁止) - 當一個對認證用戶禁止的HTTP方法被請求時
  • 410 Gone (已刪除) - 表示資源在終端不再可用。當訪問老版本API時,作爲一個通用響應很有用
  • 415 Unsupported Media Type (不支持的媒體類型) - 如果請求中包含了不正確的內容類型
  • 422 Unprocessable Entity (無法處理的實體) - 出現驗證錯誤時使用
  • 429 Too Many Requests (請求過多) - 當請求由於訪問速率限制而被拒絕時

總結

一個API是一個給開發者使用的用戶接口。要努力確保它不僅功能上可用,更要用起來愉快。

發佈了82 篇原創文章 · 獲贊 46 · 訪問量 96萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章