一篇學會 REST 深度進階
本文轉載自微信公眾號「老王Plus」,作者老王Plus的老王。轉載本文請聯系老王Plus公眾號。
說起來,REST 出現已經很久了。
從早期的三層架構,到現在的多層、微服務,核心內容之一就是 API --- 從非常簡單的 API,到多設備多用途的 API,包括一些外接的三方,像 BAT 的公共服務,簡單的、麻煩的,都是 API。而這些 API,又基本上都是基于 REST 的。
今天我們不去詳細解釋 REST,只說說 REST 應用中間的一些要點。
REST 應用之多,是有他的原因的。他很容易理解,很靈活,并且可以適用于任何規模的應用。
當然,REST 并不是唯一的規范,還有 SOAP、GraphQL。但是,這只是字面上的并列的規范。所有的規范用過了,你就會知道:SOAP 很笨重,有時候還很古怪:你需要花大量的心思去想接口的表示,而不是邏輯本身。至于 GraphQL,又延伸的太多了,居然需要調用 API 的客戶端去考慮和設計,這絕不是個好主意。
好吧,這個問題見仁見智,我們不展開討論。
不管怎么說,在我看來,REST 仍然是 API 接口規范的王者,并且不會在短時間內被取代。
在我的習慣中,使用 REST 會有以下幾個約束。
1. 使用 JSON 數據
別誤解,這是我的習慣,不是 REST 的。
REST 并沒有規定使用什么樣的格式來傳遞數據,XML 也行,JSON 也行。但是在我的團隊中,JSON 傳遞數據是一個硬性要求。
相比較而言,JSON 比 XML 有太多的優勢了:
- 更易于使用、書寫和閱讀
- 更快,占用的內存空間更少
- 不需要特殊的依賴項或包來解析
- 主流的編程語言對 JSON 都有支持
如果不理解這些優勢,沒關系,去網上隨便找一個 XML,試著自己解析一下看看。熟悉大廠的各種開放平臺的同學們也會有直覺的感覺:早期的 SOAP 和 XML,已經被逐步替換為了 REST 和 JSON。
此外,這里說的使用 JSON 數據,不僅僅是響應數據,還包括請求數據。不要使用 form-data 或者 x-www-form-urlencoded 發送數據,轉成 JSON 發送,會更容易閱讀、編寫、測試和管理。
真心的,如果你這么做了,我會替所有開發的同學們感謝你。
2. 認真對待方法
想一下,你有沒有見到過只用 GET 方法來處理一切事情的 API?
這并不是不可以,只不過,這樣的寫法說明沒有深入理解這個工具,以及 HTTP 的準確的工作方式。要知道,HTTP 中每個方法都被設計為處理特定的工作和內容。
這兒我逐個說說:
GET - 在僅僅用于讀數據時,應該用 GET。不寫入、不更新,只讀取數據。這個概念很簡單。而且,在這個前提下,相同的請求一定會返回相同的結果。
POST - 看字面的意思就明白,就是存儲一些東西,像是在數據庫中創建一條記錄、在某處寫入一些內容。通常來說,可以選擇很多種方式 POST 數據:multipart/form-data、x-www-form-urlencoded、application/json 或者 text/plain,等等,很多。不過,我們要求只使用 application/json 方式,這樣做可以保持開發和調用的一致性。
PUT - 字意就是更新內容。所以當我們需要更新數據時,就需要定義為 PUT 方法。當然,也可以用來創建新數據。
DELETE - 刪除,很好理解。
PATCH - 打補丁,對于已經存在的數據進行更新操作。這個跟 PUT 有一點點區別,通常 PATCH 是有范圍的,更新需要更新的內容,而 PUT 更多時候是更新整個數據。
當然,在某些文章里,還會有 OPTIONS、HEAD、TRACE等等,這些用得少,就不說了。想了解的,可以去查 HTTP 相關的文檔。
說這么多,重要的是 --- 既然 HTTP 提供了這樣的方法定義,我們完全可以把任何 CRUD 的操作對映到這些方法,而不是只用 GET,這決不是一個好習慣。
3. 注意語義
在團隊開發 API 時,有一個嚴格的要求,就是 API 名稱需要有語義感。語義感這個詞是我自己生造的,不是什么高大上的東西,就是要求寫的 API 名稱能使用正確的英文和次序,能夠讓人看得懂。都 9021 年了,居然還有人用拼音首字符,說出來你敢信嗎?
在我看來,所有的 API 都應該可以在不看注釋和說明的情況下被調用方理解,從調用端點,到參數,和 JSON 的鍵。
這兒,我參考了國外的一些規則。規則也很簡單:
- 用名詞,別用動詞。想一下,上面列出的方法,本身就是動詞,比方說:GET /clients,就很好理解,如果換成 GET /getClients,總覺得怪怪的。
- 一定要準確使用單數和復數,針對一條數據,就用單數;針對多條數據,就一定用復數。感覺一下 GET /client 和 GET /clients 的區別。當然,對于單個數據來說,通常還需要某種 ID 的存在:GET /client/id。
下面用一些例子來理解一下這個規則。
- // 好的方式
- GET /clients
- POST /clients
- GET /client/23
- PUT /client/23
- DELETE /client/23
- GET /client/23/comments
- // 不好的方式
- GET /clients/23
- GET /listAllClients
- POST /client/create
- PUT /updateClient/23
- GET /clientComments/23
這兒要多說兩句:規則只是規則,不用那么死板的去記。要把這種規則理解了,并習慣性地應用在編程的過程中,變成一種類似肌肉記憶的東西。
4. 隨時留心 API 的安全
就算你做得不是公開的 API,也一定要記著,使用某些手段,讓你的 API 安全起來。這是 API 編程一個基本的要求。
這又有幾個方面的要求:
1). 使用 HTTPs
HTTPs 已經出來非常久了,而且,如果你對接過大廠的 API,你會發現使用 HTTPs 是一個基本的要求。
HTTPs 提供了一種比 HTTP 更安全的方式,可以在基本網絡層面除去中間人攻擊,并加密調用端和 API 的通訊。在編程時,使用 HTTPs 是個成本最低但又確實有效的安全方式。
把使用 HTTPs 當成一個標準和習慣,有一天你會感謝自己的。
2). 從構建 API 開始,就要做到控制訪問
你看得沒錯,是從構建 API 開始。
不需要做得很麻煩,但要有控制,要能控制誰能訪問這個 API。通常可以先加入一個簡單的 JWT Auth,等 API 成形后,再轉為 OAuth。目的很簡單,就是控制訪問。如果真出現了 API 被攻擊什么的,簡單地關閉暴露的密鑰就可以了。當然,我們還可以用密鑰來跟蹤 API 的調用,包括調用量、調用異常等。
3). 小心對待敏感數據
API 代表了網絡,代表了通訊。在網絡和通訊上,傳遞敏感數據一定要小心再小心。我們前邊提到了一定使用 HTTPs,也是因為這個。如果不想面向監獄編程,一定要確保這些敏感數據通過正確的方式,給到正確的調用方。
看了一眼數據,就被追了刑責,這是我身邊的真事。
4). 確保運行環境的安全
網關、防火墻,有就用上,別因為麻煩就關掉。更深的內容,可以扔給運維,但基礎的部分,自己要懂要會。
5. 版本控制
API 疊代升級,是每個開發的會面對的事。有時候,升級僅僅是邏輯的改變,而更多時候,是會改變輸入輸出結構的。這種情況下,保持和維護 API 的版本很重要。作為后端開發人員,我們無法保證調用端會隨時同步進行相應的改動。極端情況下,改變內部邏輯,也有可能影響到調用端。
API 版本控制,不用猶豫,馬上開始使用。不要覺得某個 API 比較小,或者調用端少,就不去做。記著,任何的代碼改動,對于不更新應用或其它內容的調用者來說都是有風險的。你不僅需要確保你的代碼不會破壞任何東西或任何人,還需要知道某個應用版本的表現。這件事一點都不好玩。
關于 API 版本控制的詳細實現,我前邊一篇推文,可以去看看。傳送門
至于版本的方式,倒是不那么重要,可以看個人的習慣,v1、v2、v3也可以,v1.0、v1.1、v1.2也可以。按照微軟的建議,是采用 Major.Minor.Patch 的方式。不過我自己覺得帶上 Patch 部分有點太長了。
所以,在我的習慣中,應用版本控制后,API 的 URL會是這樣的:
- GET /v1.7/clients
- POST /v1.7/clients
- GET /v1.7/client/23
- PUT /v1.7/client/23
- DELETE /v1.7/client/23
- GET /v1.7/client/23/comments
聽我的,馬上開始 API 的版本控制。
6. 保持響應的一致
一致性是好的 API 的優秀品質。開發中,我們應該在各種方面做到一致,包括命名、URI、請求、響應等。而在這里面,響應的一致性是我對團人的一個硬性要求。
API 是要讓別人去調用的。保持資源響應的一致,是對調用者最大的善意。在某個壇子上,我看到過建議每個端點返回不同資源結構的說法。如果你也看到過類似的內容,忘了它,那是錯的。
記著這句話:保持資源響應的一致,是對調用者最大的善意。
API 開發時,盡可能發送相同的響應結構。如果沒有數據,就將其作為空值、空對象或空數據發送。
我們拿論壇的文章結構舉個例子。
文章數據的結構通常是這樣(有簡化,不要糾結):
- {
- "title": "文章標題",
- "description": "文章內容",
- "comments":
- [
- {
- "text": "回復1",
- "user": "張三"
- },
- {
- "text": "回復1",
- "user": "張三"
- }
- ]
- }
如果需要返回一條數據,并且要列出評論時,結果會是這樣:
- {
- "message": "fetch data successed",
- "status": true,
- "article":
- {
- "title": "文章標題",
- "description": "文章內容",
- "comments":
- [
- {
- "text": "回復1",
- "user": "張三"
- },
- {
- "text": "回復1",
- "user": "張三"
- }
- ]
- }
- }
如果需要返回一個文章列表,并且沒有評論時,會是這樣:
- {
- "message": "fetch data successed",
- "status": true,
- "articles":
- [
- {
- "title": "文章標題1",
- "description": "文章內容1",
- "comments": []
- },
- {
- "title": "文章標題2",
- "description": "文章內容2",
- "comments": []
- }
- ]
- }
看到了吧?這樣的方式下,我們對于里面元素 article 里結構是完全一樣的,而對于整個返回結構,也是相似的。
堅持這樣做,可以為自己和他人節省大量的時間。
7. 重視出錯后的返回信息
API 開發,應該既能處理正確的請求,也能處理錯誤的請求。錯誤的請求并不可怕,可怕的是你沒有考慮到,或者考慮到了,但沒有給到調用端足夠的細節。
在 API 返回中,很多人在這里會忽略 HTTP 的狀態代碼,也就是 HttpStatus。
HTTP 協議,為我們定義了超過 50 種不同的狀態代碼,涵蓋了幾乎所有的場景。每個代碼都有獨特的含義,應該在獨特的場景中使用。這個內容網上有很多,我就簡單列一下:
1xx - 信息性響應代碼,簡單說就是一個狀態通知。
2xx - 成功響應代碼。所有的成功都會在這個范圍。通常我們見到的是 200,但也有別的成功情況。
3xx - 重定向響應代碼。請求被服務器重定向到另一個 URL,就會有這個返回。
4xx - 客戶端錯誤響應代碼。最常見的是 400,請求協議格式或內容錯誤。
5xx - 服務器錯誤響應。最常見的是 500,服務端程序,也就是 API 的內部,有內存溢出或異常拋出。
開發中,我們可以充分并準確使用這些狀態碼。這樣,所有的開發人員,會在相同的認識層次上理解問題的狀態和原因,從而使得 API 變得普遍易懂、一致和標準。
這不是 REST 的標準,但應該作為我們開發 REST 的標準。
有了狀態碼,這只是第一步。當運行出錯時,我們需要向調用端提供盡可能多的細節。當然,這并不容易,我們需要能夠考慮并預測 API 會如何出錯,調用者會做什么,不會做什么。所以,通常一個 API 第一步是進行嚴格的請求數據驗證:數據是否存在、值是否在我們期望的范圍內、是否可以將他們存入數據庫。
拿上面的例子來說,GET /client/23,取 clientId = 23 的數據,我們需要做以下的工作:
檢查請求是否有 clientId 參數,如果沒有,應該是一個 400 的狀態
檢查傳入的 clientId = 23 的記錄是否存在,如果不存在,返回響應 404
如果找到記錄,則返回響應 200
這只是一個簡單的例子,真實的編程時,需要考慮的會更多。
而且,除了狀態碼外,還要返回相應的錯誤消息,例如:輸入參數 clientId 沒有輸入、ID 為 23 的數據記錄不存在,等等。
重要的是,提供詳細的錯誤信息,可以幫助開發者和調用方了解到底什么地方發生了問題。
放心,調用者不會將這些信息顯示給最終用戶,但可以通過這些信息來快速的定位和解決問題。
8. 盡可能優化
在現代編程中,API 在體系中的角色,絕對是整個操作的大腦。所以,對于 API 的開發,最基本的要求是快速和優化,決不能讓 API 成為整個系統和生態的痛點。
要求就這么簡單。
我們可以做很多事情來確保交付一個具備良好性能和可伸縮性的 API。來看看我們能做什么?
首先是數據庫級別的優化。通常說 API 慢的時候,十有八九與數據庫有關。糟糕的數據庫設計、復雜的查詢、緩慢的硬件環境,甚至缺乏緩存,都是慢的理由。所以,開發過程中,應該隨時關注并始終優化數據庫結構、查詢、索引以及與數據庫交互的所有內容。
接下來是緩存。很多人不愿意用緩存,因為會將代碼變復雜。但是從實際效果上,越大、越復雜的系統,越應該通過緩存傳遞數據。有時候,緩存數據庫查詢能減少 100% 的加載時間。而絕大多數數據,不會進行頻繁的改變。把緩存用起來,調用端的兄弟們,會把你當親兄弟的。
另一個影響性能的因素是 API 發送到調用端的數據量。要做到確保 API 只返回調用端需要的數據,而不是全部。如果可能,不要每次都返回完整的模型細節和關系。試一下,但要與響應中的返回模型保持一致。
最后,別忘了壓縮。如果可以,使用 Brotli,或者至少也使用 Gzip 來壓縮數據。簡單的配置,可以獲得減少 50-75% 的傳輸數據,多好!
9. 做個體貼的開發者
這個要求無關技術,但我還是想寫出來。
作為一個開發人員,我們要明白,項目不是一個人的事。當我們寫完最后一行代碼,提交并合并后,你可能會認為工作已經完成。但不是,對其他很多人來說,這才是個剛剛開始。
很多人在我們完成了工作后,才能開始他們的工作。所以,我們需要以多種方式準備 API。我們要確保 API 能正常工作,要有很好的文檔,更重要的事,我們需要準備好集成支持。不過文檔寫得有多好,在集成過程中,及以后的過程中,總會有問題,各種問題。
所以,設身處地的為他人著想,盡量讓他們的工作變得容易些。構建一個良好的API,遵循我們在這里定義的規則,編寫優秀的文檔,并為所有人服務。
10. 寫完了
寫完了。
上面九條,是我團隊中執行的標準和要求。
這里我也必須說, REST 本身并不是一個標準,所以也不會有人告訴你什么是對的,什么是錯的。開發的時候多想一下:作為開發人員,我們每天都在尋找使代碼更好、更漂亮、更高效的模式,那么為什么不在 API 中也做同樣的事呢?