如何更好的設計RESTful API

知秋發表於2016-12-30

  當您的資料模型已開始穩定,您可以為您的網路應用程式建立公共API。 你意識到,很難對你的API進行重大更改,一旦它釋出,並希望儘可能得到儘可能多的前面。 現在,網際網路對API設計的意見有很多。 但是,因為沒有一個廣泛採用的標準在所有情況下都有效,所以你前面有一堆選擇:你應該接受什麼格式? 你應該如何認證? 你的API是否應該版本化?

  構建API是您可以做的最重要的事情之一,以提高您的服務的價值。 通過使用API,您的服務/核心應用程式有可能成為其他服務增長的平臺。 看看當前巨大的科技公司:Facebook,Twitter,谷歌,GitHub,亞馬遜,Netflix …沒有一個人會像今天一樣大,如果他們沒有通過API開啟他們的資料。 事實上,整個行業存在的唯一目的是消費由所述平臺提供的資料。

你的API越簡單明瞭,使用的人就越多。

  許多在網路上發現的API設計觀點是圍繞主觀的模糊標準解釋的學術討論,而不是在現實世界中有意義的。 我的目標是描述一個為當今的Web應用程式設計的務實的API的最佳實踐。 我沒有嘗試滿足一個標準,如果它不覺得正確。 為了幫助指導決策過程,我寫了一些API必須努力達到的要求:

  • 它應該使用Web標準,他們有意義
  • 它應該對開發人員友好,可以通過瀏覽器位址列探索
  • 它應該簡單,直觀和一致,使採用不僅容易,而且愉快
  • 它應該提供足夠的靈活性來支援大部分我們所設計的UI
  • 應該是有效的,同時保持與其他要求的平衡

  API是開發人員的UI - 就像任何UI一樣,確保使用者的體驗被仔細考慮是非常重要的!

  RESTful API設計定義

  以下是我將在本文件中使用的一些重要術語:

  • Resource :物件的單個例項。 例如,一隻動物。
  • 集合:物件的集合。 例如,動物。
  • HTTP :用於通過網路通訊的協議。
  • Consumer :能夠發出HTTP請求的客戶端計算機應用程式。
  • 第三方開發人員:不是您專案的一部分,但希望使用您的資料服務的開發人員。
  • 伺服器:可通過網路從客戶端訪問的HTTP伺服器/應用程式。
  • 端點:伺服器上的API網址,表示資源或整個集合。
  • 冪等:無邊際效應,多次操作得到相同的結果。
  • 網址區段:網址中的斜線分隔的資訊。

  資料設計和抽象

  首先將從你寫的開發文件API開始(比如我們可以看到各個開發平臺的暴露出來的API文件),您需要決定如何設計資料,以及您的核心服務/應用程式如何工作。 如果你在做的API是第一次開發,這應該很容易。 如果您要將API附加到現有專案,則可能需要提供更多抽象(畢竟是要按照已有的文件規範來做)。

  有時,集合可以表示資料庫表,資源可以表示該表中的一行。 然而,這不是通常的情況。 事實上,你的API應該儘可能多地抽象出你的資料和業務邏輯。 非常重要的一點是,如果您不希望使用你的API很難使用,就不要使用任何複雜的應用程式資料來為難第三方開發人員(讓開發人員覺得還得對這些資料進一步處理而浪費更多精力)。

  還有你的服務的很多部分,你不應該通過API公開。 一個常見的例子是許多API不允許第三方建立使用者。

  設計資源請求

  當然你知道GET和POST請求。當您的瀏覽器訪問不同的網頁時,這兩個最常用的請求。POST是如此受歡迎,它甚至流行語我們的平常的說話中,即使那些不知道網際網路如何工作的人也知道他們可以“釋出”的東西在朋友的Facebook上。

  有四個半非常重要的HTTP動詞,你需要知道。我說“一半”,因為PATCH動詞非常類似於PUT動詞,兩個通常由許多API開發人員組合。這裡是動詞,在他們旁邊是他們相關的資料庫呼叫(我假設大多數人讀這個知道更多關於寫入資料庫而不是設計一個API)。

  • GET (SELECT):從伺服器檢索特定資源,或資源列表。
  • POST (CREATE):在伺服器上建立一個新的資源。
  • PUT (UPDATE):更新伺服器上的資源,提供整個資源。
  • PATCH (UPDATE):更新伺服器上的資源,僅提供更改的屬性。
  • DELETE (DELETE):從伺服器刪除資源。

  這裡有兩個較少知名的HTTP動詞:

  • HEAD - 檢索有關資源的後設資料,例如資料的雜湊或上次更新時間。
  • OPTIONS - 檢索關於客戶端被允許對資源做什麼的資訊。

  一個好的RESTful API將使用四個半HTTP動詞,允許第三方與其資料進行互動,並且不會將動作/動詞作為URL段。

  通常,GET請求可以被快取(通常是!)在瀏覽器,例如將快取請求頭用於第二次使用者的POST請求。 HEAD請求基本上是一個沒有響應主體的GET,並且也可以被快取。

  版本控制

  無論你正在構建什麼,無論你事先做了多少規劃,你的核心應用程式總會改變,你的資料關係總會改變,屬性新增和從你的資源中刪除。這只是軟體開發的工作原理,尤其是如果你的專案還活著並被許多人使用(如果你正在構建一個API,情況可能就會如此)。

  記住,API是伺服器和客戶端之間的已釋出約定。如果您更改了伺服器API,這些更改會破壞向後相容性,那麼你就打破了這個約定,客戶端又會要求你重新支援它(誰讓客戶端依然是之前的版本,呼叫的還是之前的API)。為了避免這樣的事情,並讓您的客戶端滿意,您需要偶爾引入新版本的API,同時仍允許訪問舊版本。

  注意,如果你只是為你的API新增新的特性,例如資源上的新屬性,或者如果你新增新的端點(比如之前只有查詢,現在增加一個修改),你不需要增加您的API版本號,因為這些更改不會破壞向後相容性。當然,您將需要更新您的API文件。

  隨著時間的推移,您可以棄用API的舊版本。棄用某個功能並不意味著關閉它或者降低它的質量,而是告訴客戶端您的API,舊版本將在特定日期刪除,並且他們應該升級到較新的版本。

  一個好的RESTful API設計將跟蹤URL中的版本。另一個最常見的解決方案是將版本號放在請求頭中,但在與許多不同的第三方開發人員合作之後,我可以告訴您,新增這些請求頭資訊並不像新增網址細分那麼容易。

  分析

  跟蹤客戶端使用的API的版本/端點。 這可以像每次請求時在資料庫中增加一個整數一樣簡單。 有很多原因跟蹤API Analytics是一個好主意,例如,最常用的API呼叫應該是高效的。

  為了構建第三方開發者所喜歡的API,最重要的是,當您棄用某個版本的API時,實際上可以使用已棄用的API功能與開發人員聯絡(在兩個異構系統中當對方的開發人員呼叫本服務時順帶告知對方)。 這是提醒他們在棄用舊API版本之前升級的完美方法。

  第三方開發者通知的過程可以自動化,例如。 每當對一個已棄用的功能發出10,000個請求時,發郵件通知開發人員。

  API Root URL

  無論你相信與否,您的API的根位置是重要的。當開發人員使用您的API接手舊專案並需要構建新功能時,他們可能根本不知道您有哪些服務。幸好他們知道客戶端對外呼叫的那些URL列表。重要的是,進入您的API的根入口點儘可能簡單,因為長的複雜URL將顯得令人生畏,並可能使開發人員直接略過而不會採用。

  這裡有兩個常見的URL根:

  • https://example.org/api/v1/*
  • https://api.example.com/v1/*

  如果您的應用程式龐大,或者您預計它會變得龐大,將API放在自己的子域(例如 api。)上是一個不錯的選擇。這可以允許在路上一些更靈活的可擴充套件性。

  如果您預計您的API將不會增長到那麼大,或者您想要一個更簡單的應用程式設定(例如,您希望從同一個框架託管網站和API),將您的API放置在域根的URL段(例如 / api / )也有效。

  將內容設為您的API根目錄是個好主意。例如,點選GitHub的API的根會返回一個端點列表。就個人而言,我喜歡使用根網址提供給開發人員認為有用的資訊,例如,如何獲取API的開發人員文件。

  此外,請注意HTTPS字首。作為一個好的RESTful API,您必須在HTTPS之後託管您的API(一個好的RESTful API總是基於HTTPS來發布的)。

  端點

  端點是您的API中指向特定資源或資源集合的URL。

  如果你正在構建一個虛擬的API來代表幾個不同的動物園,每個動物園包含許多動物,員工(可以在多個動物園工作)和跟蹤每個動物的物種,你可能有以下端點:

  • https://api.example.com/v1/**zoos**
  • https://api.example.com/v1/**animals**
  • https://api.example.com/v1/**animal_types**
  • https://api.example.com/v1/**employees**

  當引用每個端點可以做什麼時,您需要列出有效的HTTP動詞和端點組合。例如,這裡有一個半全面的行動列表,可以使用我們虛構的API執行。請注意,我在每個端點之前都有HTTP動詞,因為這是在HTTP請求標頭中使用的相同符號。

  • GET / zoos:列出所有動物園(ID和名稱,不要太多細節)
  • POST / zoos:建立一個新的Zoo
  • GET / zoos / ZID:檢索整個Zoo物件
  • PUT / zoos / ZID:更新Zoo(整個物件)
  • PATCH / zoos / ZID:更新Zoo(部分物件)
  • DELETE / zoos / ZID:刪除動物園
  • GET / zoos / ZID / animals:檢索動物列表(ID和名稱)。
  • GET / animals:列出所有動物(ID和名稱)。
  • POST / animals:建立一個新的動物
  • GET / animals / AID:檢索動物物件
  • PUT / animals / AID:更新動物(整個物件)
  • PATCH / animals / AID:更新動物(部分物件)
  • GET / animal_types:檢索所有動物型別的列表(ID和名稱)
  • GET / animal_types / ATID:檢索整個動物型別物件
  • GET / employees:檢索完整的員工列表
  • GET / employees / EID:檢索特定員工
  • GET / zoos / ZID / employees:檢索在此動物園工作的員工(ID和名稱)的列表
  • POST / employees:建立一個新員工
  • POST / zoos / ZID / employees:在特定動物園僱用員工
  • DELETE / zoos / ZID / employees / EID:從特定的動物園中解僱員工

  在上面的列表中,ZID表示Zoo ID,AID表示動物ID,EID表示Employee ID,ATID表示動物型別ID。在你的文件中有一個鍵,你選擇的任何約定是一個好主意。

  為了簡潔,我在上面的示例中省略了常見的API網址字首。雖然這在通訊期間可能很好,但在實際的API文件中,您應該始終顯示每個端點的完整網址(例如GET http://api.example.com/v1/animal_type/ATID)。

  注意資料之間的關係如何顯示,特別是僱員和動物園之間的多對多關係。通過新增其他網址細分,您可以執行更具體的互動。當然,對於“FIRE(解僱)”沒有HTTP動詞,但是通過對位於Zoo內的Employee執行DELETE,我們能夠實現相同的效果。

  過濾器

  當客戶端請求物件列表時,請務必為它們提供符合所請求條件的每個物件的列表。這個列表可能是巨大的。但是,重要的是不要對資料執行任何任意限制。正是這些任意的限制使第三方開發者很難知道發生了什麼。如果他們請求某個集合,並迭代結果,他們從來沒有看到超過100個結果,接下來他們就不得不去查詢這個限制條件的出處(提供服務端沒有問題,就只能是呼叫端的問題了)。到底是他們的ORM的bug導致的,還是因為網路截斷了大資料包?

儘可能減少那些會影響到第三方開發者開發的無謂限制

  然而,重要的是,您確實為客戶端提供了指定某種過濾/結果限制的能力。這麼做最重要的一個原因是可以最小化網路傳輸,客戶端儘快得到結果。第二個重要的原因是客戶端可能是懶惰的,如果伺服器可以為他們做過濾和分頁,一切都更好。還有一個不那麼重要的原因,請求資源越少,對伺服器的一個很大的好處是,減少了負載。

  過濾主要用於對資源集合執行GET。由於這些是GET請求,因此應通過URL傳遞過濾資訊。以下是您可能想要新增到API的過濾型別的一些示例:

  • ?limit = 10:減少返回給Consumer的結果數(用於分頁)
  • ?offset = 10:向客戶端傳送資訊集(用於分頁)
  • ?animal_type_id = 1:過濾符合以下條件的記錄(WHERE animal_type_id = 1)
  • ?sortby = name&order = asc:根據指定的屬性對結果進行排序(ORDER BYname ASC)

  其中一些過濾可能與端點URLS冗餘。例如我之前提到的GET / zoo / ZID / animals。這與GET / animals是一樣的嗎?zoo_id = ZID。為客戶端提供的專用端點將使他們的開發更輕鬆,這對於您預期他們會做很多的請求尤其如此。在文件中,提及這種冗餘,以便第三方開發人員不會留意是否存在差異。

  還有一個要說的是,每當您執行資料的過濾或排序時,請確保您列出客戶端可以過濾和排序的列。我們不希望將任何資料庫錯誤傳送給客戶端!

  狀態碼

  作為RESTful API,使用正確的HTTP狀態程式碼非常重要;他們是一個標準!各種網路裝置能夠讀取這些狀態碼,例如,負載平衡器可以配置為避免向傳送大量50x錯誤的Web伺服器傳送請求。有很多HTTP狀態程式碼可供選擇,但此列表應該是一個很好的起點:

  • 200 OK - [GET]
  • 客戶端從伺服器請求資料,伺服器為它們找到它(等冪)
  • 201 CREATED - [POST / PUT / PATCH]
  • 客戶端提供了伺服器資料,並且伺服器建立了一個資源
  • 204 無內容 - [刪除]
  • 客戶端要求伺服器刪除資源,並且伺服器將其刪除
  • 400 無效請求 - [POST / PUT / PATCH]
  • 客戶端給伺服器的資料不良,伺服器沒有做任何事情(冪等)
  • *錯誤404 - []
    *客戶端引用了一個不存在的資源或集合,並且伺服器什麼也不做(冪等)
    
  • *500內部伺服器錯誤 - []
    *伺服器遇到錯誤,並且客戶端不知道請求是否成功
    

  狀態碼範圍

  1xx 範圍保留用於底層HTTP的東西,你很可能永遠也用不到。

  2xx 範圍保留用於成功訊息,儘可能確保您的伺服器儘可能多地向客戶端傳送這些訊息。

  3xx 範圍保留用於重定向。大多數API不使用這些請求很多(不像SEO人使用它們那麼頻繁),然而,較新的超媒體風格API將更多地使用這些請求。

  4xx 範圍保留用於響應客戶端做出的錯誤,例如。他們提供不良資料或要求不存在的東西。這些請求應該是冪等的,而不是更改伺服器的狀態。

  5xx 範圍的狀態碼是保留給伺服器端錯誤用的。這些錯誤常常是從底層的函式丟擲來的,甚至開發人員也通常沒法處理,傳送這類狀態碼的目的以確保客戶端獲得某種響應。當收到5xx響應時,客戶端不可能知道伺服器的狀態,所以這類狀態碼是要儘可能的避免。

  預期的返回文件

  當使用不同的HTTP動詞對伺服器端點執行操作時,客戶端需要在返回結果裡面拿到一系列的資訊。

  下面的列表是非常典型的RESTful API:

  • GET / collection:返回資源物件的列表(陣列)
  • GET / collection / resource:返回單個Resource物件
  • POST / collection:返回新建立的Resource物件
  • PUT / collection / resource:返回完整的Resource物件
  • PATCH / collection / resource:返回完整的Resource物件
  • DELETE / collection / resource:返回一個空文件

  請注意,當Consumer建立資源時,他們通常不知道正在建立的資源的ID(也不知道其他屬性,如建立和修改的時間戳)(如果適用)。 這些附加屬性與後續請求一起返回,當然作為對初始POST的響應。

  ###認證

  大多數時候,一個伺服器想要知道誰正在做哪些請求。當然,一些API提供公共使用者(匿名使用者)使用的,但大多數時間的工作是代表某人執行。

  OAuth 2.0提供了一個很好的方法。對於每個請求,您可以確定知道哪個客戶正在發出請求,代表他們請求哪個使用者,並提供一種(大部分)標準化的方式來過期訪問或允許使用者撤消來自客戶端的訪問權,需要第三方客戶端知道使用者登入憑據。

  還有OAuth 1.0xAuth同樣適用這樣的場景。無論您選擇哪種方法,請確保它是常見的,並且有許多不同的庫為您的客戶端可能使用的語言/平臺編寫的文件(比如redis提供Java呼叫的API)。

  我可以誠實地告訴你,OAuth 1.0a,雖然它是最安全的選項,但是實現起來很痛苦。建議你選擇一個替代品。

  內容型別

  目前,最令人興奮的API提供來自RESTful介面的JSON資料。這包括Facebook,Twitter,GitHub,你命名。 XML似乎已經失去了優勢(除了在大型企業環境中)。 SOAP,不幸的是,它過時了,我們真的沒有看到太多的API把HTML作為結果返回給客戶端(除非你在構建一個爬蟲程式)。

  只要你返回給他們有效的資料格式,開發者就可以使用流行的語言和框架進行解析。如果你正在構建一個通用的響應物件並使用不同的序列化器,你也可以很容易的提供之前所提到的那些資料格式(不包括SOAP)。而你所要做的就是把使用方式放在響應資料的接收頭裡面。

  一些API建立者建議向URL(端點之後)新增.json,.xml或.html副檔名以指定要返回的內容型別,但我個人不喜歡這一點。我真的很喜歡Accept頭(它是內建在HTTP規範),並且我覺得這麼做也比較適當一些。

  超媒體API

  超媒體API很可能是RESTful API設計的未來。 實際上是一個非常好的概念,它迴歸到了HTTP和HTML如何運作的“本質”。

  當使用非超媒體RESTful API時,URL端點是伺服器和使用者之間的約定的一部分。這些端點必須由客戶端提前知道,並且更改這些端點意味著客戶端不再能夠按預期與伺服器通訊。你可以先假定這是一個限制。

  現在,API客戶端已經不僅僅只有那些建立HTTP請求的使用者代理了。大多數HTTP請求是由人們通過瀏覽器產生的。人們不會被哪些預先定義好的RESTful API端點URL所約束。是什麼讓人們變的如此與眾不同?人們可以閱讀內容,點選連結,看看有趣的標題,一般來說,探索一個網站,解釋內容,去他們想去的地方。即使一個URL改變,人們也不受影響(除非,他們事先給某個頁面做了書籤,在這種情況下,他們去主頁並發現原來有一條新的路徑可以去往之前的頁面)。

  超媒體API概念的工作方式與人類相同。請求API的根返回一個URL列表,它可能指向每個資訊集合,並以客戶端可以理解的方式描述每個集合。為每個資源提供ID並不重要(或必需),只要提供了一個URL即可。

  隨著超媒體API的客戶端爬行連結和收集資訊,URL在響應中始終是最新的,並且不需要事先知道作為約定的一部分。如果URL被快取,並且後續請求返回404,則客戶端可以簡單地返回到根並再次發現內容。

  在檢索集合中的資源列表時,將返回包含各個資源的完整URL的屬性。當執行POST / PATCH / PUT時,響應可以是3xx重定向到完整的資源。

  JSON不僅告訴了我們需要定義哪些屬性作為URL,也告訴了我們如何將URL與當前文件關聯的語義。正如你猜的那樣,HTML就提供了這樣的資訊。我們可能很樂意看到我們的API走完了完整的週期,並回到了處理HTML上來。想一下我們與CSS一起前行了多遠,有一天我們甚至可能會看到,API和網站使用完全相同的URL和內容是常見的做法。

  文件

  老實說,即便你不能百分之百的遵循指南中的條款,你的API不一定是糟糕的。但是,如果你不為API準備文件的話,沒有人會知道如何使用它,那它真的會成為一個糟糕的API。

  使您的文件可用於未經身份驗證的開發人員。

  不要使用自動文件生成器,或者如果你這樣做,你也要保證自己審閱過並使其具有更好的版式。

  不要截斷示例請求和響應正文,要展示完整的東西。在文件中使用語法高亮指示符。

  記錄每個端點的預期響應程式碼和可能的錯誤訊息,以及導致這些錯誤訊息可能出現的錯誤。

  如果您有空閒時間,請構建一個開發人員API控制檯,以便開發人員可以立即試用您的API。這不像你想象的那麼難,開發者(內部和第三方)也會因此而擁戴你!

  確保您的文件可以列印; CSS是一個強大的東西;不要害怕在列印文件時隱藏側邊欄。即使沒有人列印過物理副本,你會驚奇的發現有多少開發者喜歡列印到PDF以供離線閱讀。

  勘誤:原始的HTTP封包

  因為我們所做的一切都是通過HTTP,我將向你展示一個HTTP包的剖析。 我經常感到驚訝的是,有多少人不知道這些東西是什麼樣子的! 當客戶端向伺服器傳送請求時,它們提供一組鍵/值對,稱為標題,以及兩個換行符,最後是請求體。 這都是在同一個資料包中傳送的。

  伺服器然後以所述鍵/值對格式,用兩個換行符然後響應主體進行響應。 HTTP是一個請求/響應協議; 沒有“推送”支援(伺服器向客戶端傳送資料未經安全),除非您使用不同的協議,如Websockets。

  在設計API時,您應該能夠使用允許檢視原始HTTP資料包的工具。 例如,考慮使用Wireshark。 此外,請確保您使用的框架/ Web伺服器,允許您閱讀和更改儘可能多的這些欄位。

  Example HTTP Request

POST /v1/animal HTTP/1.1
Host: api.example.org
Accept: application/json
Content-Type: application/json
Content-Length: 24

{
  "name": "Gir",
  "animal_type": 12
}

  Example HTTP Response

HTTP/1.1 200 OK
Date: Wed, 18 Dec 2013 06:08:22 GMT
Content-Type: application/json
Access-Control-Max-Age: 1728000
Cache-Control: no-cache

{
  "id": 12,
  "created": 1386363036,
  "modified": 1386363036,
  "name": "Gir",
  "animal_type": 12
}

  參考:

  Principles of good RESTful API Design - Code Planet

  Best Practices for Designing a Pragmatic RESTful API

相關文章