01.30 API設計的七宗罪


API設計的七宗罪

Learning from bad examples

我當時正在幫助一個需要將物業管理系統中的房屋可用性與客戶網站集成的朋友。 幸運的是,物業管理系統具有API。 而不幸的是,這一切都錯了。

這個故事的目的不是給二手系統帶來不好的廣告,而是分享不應該開發的東西,以及在設計API時學習正確的方法。

任務

我朋友的客戶正在使用Beds24系統管理他們的財產清單,並在各種預訂系統(預訂,AirBnB等)之間保持可用性同步。 他們正在建立一個網站,並希望搜索機制僅顯示可用於所選日期和客人人數的屬性。 聽起來像是一項艱鉅的任務,因為Beds24提供了與其他系統集成的API。 不幸的是,開發人員在設計時已經犯了很多錯誤。 讓我們逐步解決這些錯誤,看看到底出了什麼問題以及應該如何解決。

一宗罪:請求正文格式

由於只對獲取客戶端屬性的可用性感興趣,因此僅對/ getAvailabilities調用感興趣。 即使這是獲取可用性的調用,但實際上這是一個POST請求,因為作者決定接受過濾器作為JSON正文。 以下是所有可能參數的列表:

<code>{
"checkIn": "20151001",
"lastNight": "20151002",
"checkOut": "20151003",
"roomId": "12345",
"propId": "1234",
"ownerId": "123",
"numAdult": "2",
"numChild": "0",
"offerId": "1",
"voucherCode": "",
"referer": "",
"agent": "",
"ignoreAvail": false,
"propIds": [
1235,
1236
],
"roomIds": [
12347,
12348,
12349
]
}/<code>


讓我遍歷JSON對象並解釋其參數出了什麼問題。

· 日期checkIn,lastNight和checkOut使用YYYYMMDD格式設置。 將日期編碼為字符串時,絕對沒有理由不使用標準ISO 8601格式(YYYY-MM-DD),因為這是大多數開發人員和JSON解析器都理解並期望的廣泛採用的標準。 除此之外,lastNight字段似乎是多餘的,因為提供了checkOut,它總是比前一天晚一天。 請始終使用標準日期編碼格式,不要要求API用戶提供冗餘數據。

· 所有Id以及numAdult和numChild字段都是數字,但被編碼為字符串。 在這種特殊情況下,似乎沒有理由將它們編碼為字符串。

· 我們具有以下字段對:roomId和roomIds以及propId和propIds。 因為可以使用roomIds和propIds傳遞ID,所以具有roomId和propId屬性不僅是多餘的,而且這裡還存在類型問題。 請注意,roomId需要一個字符串,而roomIds需要一個數字數組。 這可能會造成混亂,解析問題,並且意味著即使我們在談論相同的數據,後端本身也會對字符串執行某些操作,並對數字執行某些操作。

請不要將開發人員犯此類愚蠢的錯誤,並嘗試使用標準格式,並注意冗餘和字段類型。 不要只是將所有內容包裝在字符串中。

二宗罪:響應正文格式

如上一部分有關請求正文格式的說明,我們僅關注/ getAvailabilities調用。 這次讓我們看一下響應主體格式,看看有什麼問題。 請記住,我們有興趣獲取給定日期和許多客人可用的屬性的ID。 以下是相應的請求和響應正文:

請求:

<code>{
"checkIn": "20190501",
"checkOut": "20190503",
"ownerId": "25748",
"numAdult": "2",
"numChild": "0"
}/<code>

響應:

<code>{
"10328": {
"roomId": "10328",
"propId": "4478",
"roomsavail": "0"
},
"13219": {
"roomId": "13219",
"propId": "5729",
"roomsavail": "0"
},
"14900": {
"roomId": "14900",
"propId": "6779",
"roomsavail": 1
},
"checkIn": "20190501",
"lastNight": "20190502",
"checkOut": "20190503",
"ownerId": 25748,
"numAdult": 2
}/<code>


· ownerId和numAdult在響應中突然變成數字,而不是在請求正文中為字符串。

· 沒有屬性列表。相反,屬性是使用roomId作為鍵的頂級對象。這意味著,為了獲取可用屬性的列表,我們需要遍歷所有對象,檢查是否存在某些參數(例如roomsavail),捨棄其他參數(例如checkIn,lastNight等)以獲取屬性列表。然後,我們需要檢查roomsavail屬性的值,如果該值大於0,則將該屬性視為可用。但是請稍等,在這裡仔細查看:" roomsavail":" 0",在這裡" roomsavail":1.看到模式了嗎?如果沒有可用的房間,則值為字符串,而如果有可用的房間,則為數字!這將在諸如Java之類的強制執行類型安全的語言中引起很多問題,因為同一屬性不應屬於不同的類型。請使用適當的JSON列表來顯示數據集合,而不要像上面那樣使用奇怪的鍵值構造,並確保不要在對象之間更改字段類型。正確格式的響應如下所示(請注意,這種格式也可以在不重複任何內容的情況下獲取有關房間的信息):

<code>{
"properties": [
{
"id": 4478,
"rooms": [
{
"id": 12328,
"available": false
}
]
},
{
"id": 5729,
"rooms": [
{
"id": 13219,
"available": false
}
]
},
{
"id": 6779,
"rooms": [
{
"id": 14900,
"available": true
}
]
}
],
"checkIn": "2019-05-01",
"lastNight": "2019-05-02",
"checkOut": "2019-05-03",
"ownerId": 25748,
"numAdult": 2
}/<code>


三宗罪:錯誤處理

此API中的錯誤處理是通過以下方式實現的:即使發生錯誤,所有請求也會返回200的響應代碼。 這意味著除了解析正文並檢查是否存在error或errorCode字段之外,沒有其他方法可以區分響應是成功還是失敗。 該API僅包含6個錯誤代碼,如下所示:

API設計的七宗罪

Error codes of Beds24 API

請考慮在出現問題時不要使用這種返回響應代碼200(成功)的方法,除非這是在API框架中使用的標準方法。 優良作法是使用大多數客戶端和開發人員都可以識別的標準HTTP錯誤代碼。 例如,以上屏幕快照中的錯誤代碼1009應該替換為401(未經授權)HTTP代碼。 如果API客戶端可以預先知道是否解析主體以及如何解析主體(作為數據對象或錯誤對象),則將使工作變得更輕鬆。 如果錯誤是特定於應用程序的,則最好在響應正文中返回400(錯誤請求)或500(服務器錯誤)以及相應的錯誤消息。

對於給定的API選擇哪種錯誤處理策略,只要確保它是一致的並符合廣泛採用的HTTP標準即可。 這將使我們的生活更輕鬆。

四宗罪:"準則"

以下是文檔中API使用的"準則":

使用API時請遵守以下準則

1.呼叫應設計為僅發送和接收所需的最少數據。

2.一次僅允許一個API調用,您必須等待第一個調用完成才能開始下一個API調用。

3.多個呼叫之間的間隔應間隔幾秒鐘。

4. API調用應儘量少用,並保持在合理的業務使用所需的最低限度內。

5. 5分鐘內過度使用將導致您的帳戶被封鎖,而不會發出警告。

6.我們保留自行決定禁用任何我們認為過度使用API函數的訪問的權利,恕不另行通知。

雖然第1點,第4點有意義,但我不同意其他觀點。 讓我解釋一下原因。

2.如果您正在構建REST API,則應假定該API是無狀態的,在任何時間點都不應存在任何狀態。 這是REST在雲應用程序中有用的原因之一。 如果發生故障,可以自由地重新部署無狀態組件,並且它們可以根據負載變化進行擴展。 請確保在設計RESTful API時,它實際上是無狀態的,並且開發人員無需關心"一次請求"之類的事情。

3.這是一個模稜兩可,非常奇怪的準則。 不幸的是,我無法弄清楚作者創建此"指南"的原因,但是它給人一種感覺,即在請求本身之外進行了一些處理,因此,緊接彼此進行調用可能會使系統處於某種錯誤狀態 。 同樣,作者說"幾秒鐘"的事實並沒有提供有關兩次請求之間實際時間的具體信息。

再次參見圖5和6。在這種情況下,沒有解釋什麼是"過度"。 是每秒10個請求還是1個? 此外,某些網站將擁有大量流量,並且僅由於這些原因而阻止它們使用API,而不會發出任何警告,可能會使開發人員遠離此類系統。 請在制定此類指南時具體說明,並在提出此類規則時考慮用戶。

五宗罪:文件

API文檔的外觀如下所示:

API設計的七宗罪

Beds24 API documentation look&feel

這裡唯一的問題是可讀性和整體感覺。 如果作者使用markdown而不是自定義非樣式HTML,則同一文檔的外觀可能會更好。 為了這篇文章的緣故,我在Dilliger的幫助下不到2分鐘創建了一個更好的版本。 結果如下:

API設計的七宗罪

Styled version of the documentation

請使用工具創建API文檔。 對於簡單的文檔,上面的一個markdown文件就足夠了,而對於更大,功能更豐富的文檔,最好使用Swagger或Apiary之類的工具。

這是Beds24 API文檔的鏈接,適合那些想要自己看一下的人。

六宗罪:安全

API文檔規定了關於所有端點的以下內容:

要使用這些功能,必須在菜單設置>>帳戶>>帳戶訪問中允許API訪問。

但是,實際上,任何人都可以請求獲取數據,而無需為某些調用傳遞任何憑據,例如獲取特定屬性的可用性。 在文檔的不同部分中對此進行了說明:

大多數JSON方法都需要API密鑰才能訪問帳戶。 可以在菜單>>帳戶>>帳戶訪問中設置API代碼。

除了上面關於身份驗證的溝通不暢之外,API密鑰實際上是用戶需要自己創建的東西(實際上是手動鍵入密鑰,絕不自動生成),並且其長度應在16到64個字符之間。 允許用戶自己創建密鑰可能會導致非常不安全的密鑰(很容易猜到)或某些格式問題,因為可以在帳戶設置的該字段中輸入任何字符串。 在最壞的情況下,它也可能成為SQL注入或其他類型攻擊的門戶。 請不要讓用戶創建API密鑰。 始終為他們提供不可變的自動生成的密鑰,並能夠在需要時使其無效。

對於那些實際經過身份驗證的請求,我們有一個不同的問題:身份驗證令牌必須作為請求主體的一部分發送,如下所示(從文檔中可以看出):

API設計的七宗罪

Beds24 API authentication example

在請求正文中包含身份驗證令牌意味著服務器將需要首先解析請求正文,提取密鑰,執行身份驗證,然後決定如何處理請求:是否執行請求。 如果成功通過身份驗證,則不會涉及任何開銷,因為無論如何都將解析該正文。 萬一驗證失敗,服務器將完成上述所有工作,只是提取令牌,從而浪費寶貴的處理時間。 相反,更好的方法是使用Bearer身份驗證方案或類似方法將身份驗證令牌作為請求標頭髮送。 這樣,服務器僅在成功認證的情況下才需要解析請求主體。 使用諸如Bearer令牌之類的標準身份驗證方案的另一個原因僅僅是因為大多數開發人員都熟悉它。

罪過7:性能

最後但並非最不重要的一點是,請求完成平均需要1秒鐘多一點的時間。 在現代應用中,這種延遲可能是不可接受的。 因此,在設計API時要考慮性能。


儘管上面解釋了API的所有問題,但它確實可以完成。 但是,對於開發人員來說,理解和實現它需要花費數倍的時間,並且需要花費大量時間來編寫更復雜的解決方案以解決瑣碎的問題。 因此,在發佈API之前,請考慮讓開發人員實現您的API。 確保文檔完整,清晰且格式正確。 檢查您的資源名稱是否遵循約定,數據結構是否正確,易於理解和使用。 另外,請注意安全性和性能,不要忘記正確執行錯誤處理。 如果在設計API時將上述所有因素都考慮在內,那麼就不需要像前面的示例中那樣奇怪的"準則"。

如前所述,這篇文章的目的不是讓您永遠不要使用Beds24或任何類似的系統,因為它們的API沒有正確實現。 目標是通過分享一個不好的例子並解釋如何更好地完成軟件來提高軟件產品的質量。 希望這篇文章可以使某人更多地關注軟件開發最佳實踐,並使軟件系統更好。 直到下一次!


(本文翻譯自Robert Konarskis的文章《How NOT to design APIs》,參考:https://blog.usejournal.com/how-not-to-design-restful-apis-fb4892d9057a)


分享到:


相關文章: