天天看點

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冗長,難以解析,很難讀,他的資料模型和大部分程式設計語言的資料模型 不相容,而他的可擴充性優勢在你的主要需求是必須序列化一個内部資料進行輸出展示時變得不相幹。

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

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

RESTful 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頭資訊。無論怎樣,随着迅速被采用的标準,比如CORS和Link 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 提供了一套内置的緩存架構! 所有你必須做的是,包含一些額外的出站響應頭資訊,并且在收到一些入站請求頭資訊時做一點兒校驗工作。

有兩種方式: ETag和Last-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 (請求過多) - 當請求由于通路速率限制而被拒絕時

總結

RESTful API 設計最佳實踐

戳下面的原文閱讀,更有料!