11.30 「乾貨」RESTful API 的設計原則

作者:moonz-wu
來源:https://www.cnblogs.com/moonz-wu/p/4211626.html


「乾貨」RESTful API 的設計原則


做出一個好的API設計很難。API表達的是你的數據和你的數據使用者之間的契約。打破這個契約將會招致很多憤怒的郵件,和一大堆傷心的用戶-因為他們手機上的App不工作了。而文檔化只能達到一半的效果,並且也很難找到一個願意寫文檔的程序員。

你所能做的最重要一件事來提高服務的價值就是創建一個API。因為隨著其他服務的成長,有這樣一個API會使你的服務或者核心應用將有機會變成一個平臺。環顧一下現有的這些大公司:Facebook,Twitter,Google, Github,Amazon,Netflix等。如果當時他們沒有通過API來開放數據的話,也不可能成長到如今的規模。事實上,整個行業存在的唯一目的就是消費所謂平臺上的數據。

你的API越容易使用,那麼就會有越多的人去用它。

本文提到的這些原則,如果你的API能嚴格按照這些原則來設計,使用者就可以知道它接下來要做什麼,並且能減少大量不必要的疑惑或者是憤怒的郵件。我已經把所有內容都整理到不同的主題裡了,你無需按順序去閱讀它。

定義

這裡有一些非常重要的術語,我將在本文裡面一直用到它們:

  • 資源:一個對象的單獨實例,如一隻動物
  • 集合:一群同種對象,如動物
  • HTTP:跨網絡的通信協議
  • 客戶端:可以創建HTTP請求的客戶端應用程序
  • 第三方開發者:這個開發者不屬於你的項目但是有想使用你的數據
  • 服務器:一個HTTP服務器或者應用程序,客戶端可以跨網絡訪問它
  • 端點:這個API在服務器上的URL用於表達一個資源或者一個集合
  • 冪等:無邊際效應,多次操作得到相同的結果
  • URL段:在URL裡面已斜槓分隔的內容

數據設計與抽象

規劃好你的API的外觀要先於開發它實際的功能。首先你要知道數據該如何設計和核心服務/應用程序會如何工作。如果你純粹新開發一個API,這樣會比較容易一些。但如果你是往已有的項目中增加API,你可能需要提供更多的抽象。

有時候一個集合可以表達一個數據庫表,而一個資源可以表達成裡面的一行記錄,但是這並不是常態。事實上,你的API應該儘可能通過抽象來分離數據與業務邏輯。這點非常重要,只有這樣做你才不會打擊到那些擁有複雜業務的第三方開發者,否則他們是不會使用你的API的。

當然你的服務可能很多部分是不應該通過API暴露出去的。比較常見的例子就是很多API是不允許第三方來創建用戶的。

動詞

顯然你瞭解GET和POST請求。當你用瀏覽器去訪問不同頁面的時候,這兩個是最常見的請求。POST術語如此流行以至於開始侵擾通俗用語。即使是那些不知道互聯網如何工作的人們也能“post”一些東西到朋友的Facebook牆上。

這裡至少有四個半非常重要的HTTP動詞需要你知道。我之所以說“半個”的意思是PATCH這個動詞非常類似於PUT,並且它們倆也常常被開發者綁定到同一個API上。

  • GET (選擇):從服務器上獲取一個具體的資源或者一個資源列表。
  • POST (創建): 在服務器上創建一個新的資源。
  • PUT (更新):以整體的方式更新服務器上的一個資源。
  • PATCH (更新):只更新服務器上一個資源的一個屬性。
  • DELETE (刪除):刪除服務器上的一個資源。

還有兩個不常用的HTTP動詞:

  • HEAD : 獲取一個資源的元數據,如數據的哈希值或最後的更新時間。
  • OPTIONS:獲取客戶端能對資源做什麼操作的信息。

一個好的RESTful API只允許第三方調用者使用這四個半HTTP動詞進行數據交互,並且在URL段裡面不出現任何其他的動詞。

一般來說,GET請求可以被瀏覽器緩存(通常也是這樣的)。例如,緩存請求頭用於第二次用戶的POST請求。HEAD請求是基於一個無響應體的GET請求,並且也可以被緩存的。

版本化

無論你正在構建什麼,無論你在入手前做了多少計劃,你核心的應用總會發生變化,數據關係也會變化,資源上的屬性也會被增加或刪除。只要你的項目還活著,並且有大量的用戶在用,這種情況總是會發生。

請謹記一點,API是服務器與客戶端之間的一個公共契約。如果你對服務器上的API做了一個更改,並且這些更改無法向後兼容,那麼你就打破了這個契約,客戶端又會要求你重新支持它。為了避免這樣的事情,你既要確保應用程序逐步的演變,又要讓客戶端滿意。那麼你必須在引入新版本API的同時保持舊版本API仍然可用。

注:如果你只是簡單的增加一個新的特性到API上,如資源上的一個新屬性或者增加一個新的端點,你不需要增加API的版本。因為這些並不會造成向後兼容性的問題,你只需要修改文檔即可。

隨著時間的推移,你可能聲明不再支持某些舊版本的API。申明不支持一個特性並不意味著關閉或者破壞它。而是告訴客戶端舊版本的API將在某個特定的時間被刪除,並且建議他們使用新版本的API。

一個好的RESTful API會在URL中包含版本信息。另一種比較常見的方案是在請求頭裡面保持版本信息。但是跟很多不同的第三方開發者一起工作後,我可以很明確的告訴你,在請求頭裡麵包含版本信息遠沒有放在URL裡面來的容易。

分析

所謂API分析就是持續跟蹤那些正為人使用的API的版本和端點信息。而這可能就跟每次請求都往數據庫增加一個整數那樣簡單。有很多的原因顯示API跟蹤分析是一個好主意,例如,對那些使用最廣泛的API來說效率是最重要的。

第三方開發者通常會關注API的構建目的,其中最重要的一個目的是你決定什麼時候不再支持某個版本。你需要明確的告知開發者他們正在使用那些即將被移除的API特性。這是一個很好的方式在你準備刪除舊的API之前去提醒他們進行升級。

當然第三方開發者的通知流程可以以某種條件被自動觸發,例如每當一個過時的特性上發生10000次請求時就發郵件通知開發者。

API根URL

無論你信不信,API的根地址很重要。當一個開發者接手了一箇舊項目(如進行代碼考古時)。而這個項目正在使用你的API,同時開發者還想構建一個新的特性,但他們完全不知道你的服務。幸運的是他們知道客戶端對外調用的那些URL列表。讓你的API根入口點保持儘可能的簡單是很重要的,因為開發者很可能一看到那些冗長而又複雜的URL就轉身而走。

這裡有兩個常見的URL根例子:

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

如果你的應用很龐大或者你預期它將會變的很龐大,那麼將API放到子域下通常是一個好選擇。這種做法可以保持某些規模化上的靈活性。

但如果你覺得你的API不會變的很龐大,或是你只是想讓應用安裝更簡單些(如你想用相同的框架來支持站點和API),將你的API放到根域名下也是可以的。

讓API根擁有一些內容通常也是個好主意。Github的API根就是一個典型的例子。從個人角度來說我是一個通過根URL發佈信息的粉絲,這對很多人來說是有用的,例如如何獲取API相關的開發文檔。

同樣也請注意HTTPS前綴,一個好的RESTful API總是基於HTTPS來發布的。

端點

一個端點就是指向特定資源或資源集合的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動詞和端點的組合。如下所示,請注意我把HTTP動詞都放在了虛構的API之前,正如將同樣的註解放在每一個HTTP請求頭裡一樣。(下面的URL就不翻譯了,我覺得沒啥必要翻^_^)

  • GET /zoos: List all Zoos (ID and Name, not too much detail)
  • POST /zoos: Create a new Zoo
  • GET /zoos/ZID: Retrieve an entire Zoo object
  • PUT /zoos/ZID: Update a Zoo (entire object)
  • PATCH /zoos/ZID: Update a Zoo (partial object)
  • DELETE /zoos/ZID: Delete a Zoo
  • GET /zoos/ZID/animals: Retrieve a listing of Animals (ID and Name).
  • GET /animals: List all Animals (ID and Name).
  • POST /animals: Create a new Animal
  • GET /animals/AID: Retrieve an Animal object
  • PUT /animals/AID: Update an Animal (entire object)
  • PATCH /animals/AID: Update an Animal (partial object)
  • GET /animal_types: Retrieve a listing (ID and Name) of all Animal Types
  • GET /animal_types/ATID: Retrieve an entire Animal Type object
  • GET /employees: Retrieve an entire list of Employees
  • GET /employees/EID: Retreive a specific Employee
  • GET /zoos/ZID/employees: Retrieve a listing of Employees (ID and Name) who work at this Zoo
  • POST /employees: Create a new Employee
  • POST /zoos/ZID/employees: Hire an Employee at a specific Zoo
  • DELETE /zoos/ZID/employees/EID: Fire an Employee from a specific Zoo

在上面的列表裡,ZID表示動物園的ID, AID表示動物的ID,EID表示僱員的ID,還有ATID表示物種的ID。讓文檔裡所有的東西都有一個關鍵字是一個好主意。

為了簡潔起見,我已經省略了所有API共有的URL前綴。作為溝通方式這沒什麼問題,但是如果你真要寫到API文檔中,那就必須包含完整的路徑(如,GET http://api.example.com/v1/animal_type/ATID)。

請注意如何展示數據之間的關係,特別是僱員與動物園之間的多對多關係。通過添加一個額外的URL段就可以實現更多的交互能力。當然沒有一個HTTP動詞能表示正在解僱一個人,但是你可以使用DELETE一個動物園裡的僱員來達到相同的效果。

過濾器

當客戶端創建了一個請求來獲取一個對象列表時,很重要一點就是你要返回給他們一個符合查詢條件的所有對象的列表。這個列表可能會很大。但你不能隨意給返回數據的數量做限制。因為這些無謂的限制會導致第三方開發者不知道發生了什麼。如果他們請求一個確切的集合並且要遍歷結果,然而他們發現只拿到了100條數據。接下來他們就不得不去查找這個限制條件的出處。到底是ORM的bug導致的,還是因為網絡截斷了大數據包?

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

這點很重要,但你可以讓客戶端自己對結果做一些具體的過濾或限制。這麼做最重要的一個原因是可以最小化網絡傳輸,並讓客戶端儘可能快的得到查詢結果。其次是客戶端可能比較懶,如果這時服務器能對結果做一些過濾或分頁,對大家都是好事。另外一個不那麼重要的原因是(從客戶端角度來說),對服務器來說響應請求的負載越少越好。

過濾器是最有效的方式去處理那些獲取資源集合的請求。所以只要出現GET的請求,就應該通過URL來過濾信息。以下有一些過濾器的例子,可能是你想要填加到API中的:

  • ?limit=10: 減少返回給客戶端的結果數量(用於分頁)
  • ?offset=10: 發送一堆信息給客戶端(用於分頁)
  • ?animal_type_id=1: 使用條件匹配來過濾記錄
  • ?sortby=name&order=asc: 對結果按特定屬性進行排序

有些過濾器可能會與端點URL的效果重複。例如我之前提到的GET /zoo/ZID/animals。它也同樣可以通過GET /animals?zoo_id=ZID來實現。獨立的端點會讓客戶端更好過一些,因為他們的需求往往超出你的預期。本文中提到這種冗餘差異可能對第三方開發者並不可見。

無論怎麼說,當你準備過濾或排序數據時,你必須明確的將那些客戶端可以過濾或排序的列放到白名單中,因為我們不想將任何的數據庫錯誤發送給客戶端。

狀態碼

對於一個RESTful API來說很重要的一點就是要使用HTTP的狀態碼,因為它們是HTTP的標準。很多的網絡設備都可以識別這些狀態碼,例如負載均衡器可能會通過配置來避免發送請求到一臺web服務器,如果這臺服務器已經發送了很多的50x錯誤回來。這裡有大量的HTTP狀態碼可以選擇,但是下面的列表只給出了一些重要的代碼作為一個參考:

  • 200 OK – [GET]
  • 客戶端向服務器請求數據,服務器成功找到它們
  • 201 CREATED – [POST/PUT/PATCH]
  • 客戶端向服務器提供數據,服務器根據要求創建了一個資源
  • 204 NO CONTENT – [DELETE]
  • 客戶端要求服務器刪除一個資源,服務器刪除成功
  • 400 INVALID REQUEST – [POST/PUT/PATCH]
  • 客戶端向服務器提供了不正確的數據,服務器什麼也沒做
  • 404 NOT FOUND – [*]
  • 客戶端引用了一個不存在的資源或集合,服務器什麼也沒做
  • 500 INTERNAL SERVER ERROR – [*]
  • 服務器發生內部錯誤,客戶端無法得知結果,即便請求已經處理成功

狀態碼範圍

1xx範圍的狀態碼是保留給底層HTTP功能使用的,並且估計在你的職業生涯裡面也用不著手動發送這樣一個狀態碼出來。

2xx範圍的狀態碼是保留給成功消息使用的,你儘可能的確保服務器總髮送這些狀態碼給用戶。

3xx範圍的狀態碼是保留給重定向用的。大多數的API不會太常使用這類狀態碼,但是在新的超媒體樣式的API中會使用更多一些。

4xx範圍的狀態碼是保留給客戶端錯誤用的。例如,客戶端提供了一些錯誤的數據或請求了不存在的內容。這些請求應該是冪等的,不會改變任何服務器的狀態。

5xx範圍的狀態碼是保留給服務器端錯誤用的。這些錯誤常常是從底層的函數拋出來的,並且開發人員也通常沒法處理。發送這類狀態碼的目的是確保客戶端能得到一些響應。收到5xx響應後,客戶端沒辦法知道服務器端的狀態,所以這類狀態碼是要儘可能的避免。

預期的返回文檔

當使用不同的HTTP動詞向服務器請求時,客戶端需要在返回結果裡面拿到一系列的信息。下面的列表是非常經典的RESTful API定義:

  • GET /collection: 返回一系列資源對象
  • GET /collection/resource: 返回單獨的資源對象
  • POST /collection: 返回新創建的資源對象
  • PUT /collection/resource: 返回完整的資源對象
  • PATCH /collection/resource: 返回完整的資源對象
  • DELETE /collection/resource: 返回一個空文檔

請注意當一個客戶端創建一個資源時,她們常常不知道新建資源的ID(也許還有其他的屬性,如創建和修改的時間戳等)。這些屬性將在隨後的請求中返回,並且作為剛才POST請求的一個響應結果。

認證

服務器在大多數情況下是想確切的知道誰創建了什麼請求。當然,有些API是提供給公共用戶(匿名用戶)的,但是大部分時間裡也是代表某人的利益。

OAuth2.0提供了一個非常好的方法去做這件事。在每一個請求裡,你可以明確知道哪個客戶端創建了請求,哪個用戶提交了請求,並且提供了一種標準的訪問過期機制或允許用戶從客戶端註銷,所有這些都不需要第三方的客戶端知道用戶的登陸認證信息。

還有OAuth1.0和xAuth同樣適用這樣的場景。無論你選擇哪個方法,請確保它為多種不同語言/平臺上的庫提供了一些通用的並且設計良好文檔,因為你的用戶可能會使用這些語言和平臺來編寫客戶端。

內容類型

目前,大多數“精彩”的API都為RESTful接口提供JSON數據。諸如Facebook,Twitter,Github等等你所知的。XML曾經也火過一把(通常在一個大企業級環境下)。這要感謝SOAP,不過它已經掛了,並且我們也沒看到太多的API把HTML作為結果返回給客戶端(除非你在構建一個爬蟲程序)。

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

有些API的創建者會推薦把.json, .xml, .html等文件的擴展名放在URL裡面來指示返回內容類型,但我個人並不習慣這麼做。我依然喜歡通過接收頭來指示返回內容類型(這也是HTTP標準的一部分),並且我覺得這麼做也比較適當一些。

超媒體API

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

在非超媒體RESTful API的情景中,URL端點是服務器與客戶端契約的一部分。這些端點必須讓客戶端事先知道,並且修改它們也意味著客戶端可能再也無法與服務器通信了。你可以先假定這是一個限制。

時至今日,英特網上的API客戶端已經不僅僅只有那些創建HTTP請求的用戶代理了。大多數HTTP請求是由人們通過瀏覽器產生的。人們不會被哪些預先定義好的RESTful API端點URL所束縛。是什麼讓人們變的如此與眾不同?因為人們可以閱讀內容,可以點擊他們感興趣的鏈接,並瀏覽一下網站,然後跳到他們關注的內容那裡。即使一個URL改變了,人們也不會受到影響(除非他們事先給某個頁面做了書籤,這時他們回到主頁並發現原來有一條新的路徑可以去往之前的頁面)。

超媒體API概念的運作跟人們的行為類似。通過請求API的根來獲得一個URL的列表,這個列表裡面的每一個URL都指向一個集合,並且提供了客戶端可以理解的信息來描述每一個集合。是否為每一個資源提供ID並不重要(或者不是必須的),只要提供URL即可。

一個超媒體API一旦具有了客戶端,那麼它就可以爬行鏈接並收集信息,而URL總是在響應中被更新,並且不需要如契約的一部分那樣事先被知曉。如果一個URL曾經被緩存過,並且在隨後的請求中返回404錯誤,那麼客戶端可以很簡單的回退到根URL並重新發現內容。

在獲取集合中的一個資源列表時會返回一個屬性,這個屬性包含了各個資源的完整URL。當實施一個POST/PATCH/PUT請求後,響應可以被一個3xx的狀態碼重定向到完整的資源上。

JSON不僅告訴了我們需要定義哪些屬性作為URL,也告訴了我們如何將URL與當前文檔關聯的語義。正如你猜的那樣,HTML就提供了這樣的信息。我們可能很樂意看到我們的API走完了完整的週期,並回到了處理HTML上來。想一下我們與CSS一起前行了多遠,有一天我們可能再次看到它變成了一個通用實踐讓API和網站可以去使用相同的URL和內容。

文檔

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

讓你的文檔對那些未經認證的開發者也可用

不要使用文檔自動化生成器,即便你用了,你也要保證自己審閱過並讓它具有更好的版式。

不要截斷示例中請求與響應的內容,要展示完整的東西。並在文檔中使用高亮語法。

文檔化每一個端點所預期的響應代碼和可能的錯誤消息,和在什麼情況下會產生這些的錯誤消息

如果你有富餘的時間,那就創建一個控制檯來讓開發者可以立即體驗一下API的功能。創建一個控制檯並沒有想象中那麼難,並且開發者們(內部或者第三方)也會因此而擁戴你。

另外確保你的文檔能夠被打印。CSS是個強大的工具可以幫助到你。而且在打印的時候也不用太擔心邊側欄的問題。即便沒有人會打印到紙上,你也會驚奇的發現很多開發者願意轉化成PDF格式進行離線閱讀。

勘誤:原始的HTTP封包

因為我們所做的都是基於HTTP協議,所以我將展示給你一個解析了的HTTP封包。我經常很驚訝的發現有多少人不知道這些東西。當客戶端發送一個請求道服務器時,他們會提供一個鍵值對集,先是一個頭,緊跟著是兩個回車換行符,然後才是請求體。所有這些都是在一個封包裡被髮送。

服務器響應也是同樣的鍵值對集,帶兩個回車換行符,然後是響應體。HTTP就是一個請求/響應協議;它不支持“推送”模式(服務器直接發送數據給客戶端),除非你採用其他協議,如Websockets。

當你設計API時,你應該能夠使用工具去查看原始的HTTP封包。Wireshark是個不錯的選擇。同時,你也該採用一個框架/web服務器,使你能夠在必要時修改某些字段的值。

Example HTTP Request

「乾貨」RESTful API 的設計原則

Example HTTP Response

「乾貨」RESTful API 的設計原則


分享到:


相關文章: