/users/:userName/articles/:articleId

關于這個:

/comments/:commentId
/articles/:articleId

采用這種方法的主要原因是可讀性;嵌套資源 URL 可以傳達一個資源屬于另一個資源的信息。它呈現出一種層次關系,就像文件系統中的目錄一樣。

這些 URL 傳達的關系含義較少:

/books/:bookId
/rating/:ratingId

比這些 URL:

/books/:bookId
/books/:bookId/ratings/:ratingId

我們可以直接看到我們請求的評分屬于某本特定的書。在很多情況下,這可以使調試更容易。

之所以說是層次關系,是因為底層數據模型不一定是層次化的。例如,在 GitHub 上,一個用戶可以為多個存儲庫貢獻代碼,而一個存儲庫可以有來自不同用戶的貢獻。這是一種多對多關系。

/users/:userName/repos
/repos/:repoName/users

如果您只知道其中一個端點,那么它可能看起來是一對多的關系。

其他更技術性的原因是嵌套資源的相對 ID上下文。

例如,房子有門牌號,但這些門牌號只與它們所屬的街道有關。如果你知道房子的門牌號是 42,但你不記得街道,那么這對你沒有多大幫助。

/street/:streetName/house/:houseNumber

另一個例子是文件系統中的文件名。如果數百個不同目錄中有數百個文件以同樣的名字命名,那么僅僅知道我們的文件叫什么名字是沒有用的。

/home/kay/Development/project-a/README.md
/home/kay/Development/project-b/README.md

如果我們使用關系數據庫,我們通常對所有數據記錄都有唯一的鍵,但正如我們所見,對于其他類型的數據存儲(如文件系統),情況不一定如此。

嵌套 URL 也很容易操作。如果 URL 中編碼了層次結構,我們可以刪除 URL 的部分內容以向上爬升此層次結構。這使得帶有嵌套資源的 API 導航變得相當簡單。

總而言之,我們希望使用嵌套資源來提高可讀性,進而改善開發人員體驗,有時我們甚至必須使用它們,因為數據源不提供僅通過其 ID 來識別嵌套資源的方法。

缺點

現在我們討論了為什么應該使用嵌套的原因,討論另一方面也很重要:為什么我們不應該嵌套我們的資源?

雖然筑巢有時是必要的并且無法避免,但它通常是一個我們應該牢記的特定成本或危險的選擇。

讓我們逐一看一下。

可能很長的 URL

我們之前了解到嵌套資源可以使我們的 URL 更具可讀性,但這并不是萬無一失的。

特別是在資源之間存在多種關系的相當復雜的系統中,嵌套方法可能會導致相當長且復雜的 URL。

/customers/:customerId/projects/:projectId/orders/:orderId/lines/:lineId

如果我們使用長字符串作為 ID,這個問題可能會變得更加嚴重:

/customers/8a007b15-1f39-45cd-afaf-fa6177ed1c3b/projects/b3f022a4-2970-4840-b9bb-3d14709c9d2a/orders/af6c1308-613f-40ff-9133-a6b993249c88/lines/3f16dca9-870e-4692-be2a-ea6d883b9dfd

因此,當我們開始沿著這條路走下去時,我們應該有時退后一步,看看我們是否仍然能夠實現提高可讀性的目標。

經驗法則是最大嵌套深度為 2。有時深度為 3 也是可以的。例如,如果我們的 ID 很短且易于閱讀。

/author/kay-ploesser/book/react-from-zero/review/23

冗余端點

一般來說,使用嵌套資源不如僅使用根資源那么靈活。

例如,如果我們有多對多關系。存儲庫有多個貢獻者,但每個用戶也可以為各種存儲庫做出貢獻。

如果我們想通過嵌套資源實現這一點,我們必須為這種關系單獨創建兩個端點

/user/:userName/repositories
/repositories/:repositoryName/contributors

如果我們想在不嵌套的情況下實現這一點,我們可以為貢獻定義一個根資源,該資源還允許在其 URL 中使用過濾參數。

/contributions?userName=:userName&repositoryName=:repositoryName

參數是可選的,所以我們也可以用它來獲取所有貢獻,并且我們可以用PUTPOST來改變和創建關系。

雖然這似乎不是一對多關系的問題,其中關系的一部分不能有多個連接,但我們仍然可以在某個時候搜索嵌套資源在其父資源中的所有記錄。

因此,當有這個端點時:

/mothers/:motherName/children

我們可能仍然想獲取所有母親的所有孩子,并為此創建一個新的端點

/children

冗余端點也會增加我們的 API 的表面,雖然資源關系中更易讀的 URL 對開發人員體驗來說是一件好事,但大量的端點卻不是。

多個端點增加了 API 所有者記錄整個過程的努力,并使新客戶的入職變得更加麻煩。

返回相同表示的多個端點也可能導致緩存問題,并可能違反RESTful API 設計的核心原則之一。

這個問題可以通過 HTTP 重定向來解決,因此所有表示都從中央根資源返回并可以緩存,但仍然需要代碼來實現這一點。

它還可能違反另一個核心原則,即統一接口。

當客戶端持有資源的表示(包括附加的任何元數據)時,它就擁有足夠的信息來修改或刪除服務器上的資源,前提是它有這樣做的權限。

如果表示不包含有關嵌套的信息,并且我們沒有根資源來直接訪問它;我們就無法創建、更新或刪除它。

多個數據庫查詢

如果我們向下遍歷關系圖而不是使用一個唯一標識符(如果存在)來從資源中檢索表示,則我們需要檢查 URL 中實現的關系是否成立。

以獲取嵌套評論為例

/blogs/X/articles/Y/comments/Z

獲取所有博客所有文章的所有評論也是一個問題。

  1. 查詢所有博客
  2. 查詢每個博客的每篇文章
  3. 查詢每篇文章的每條評論

此 API 設計給我們帶來了N+1 查詢問題的巨大困擾。

如果我們只有評論的根資源,我們可以查詢它并在需要時添加一些過濾參數。如果評論具有全局唯一 ID,我們可以直接查詢它們。

/comments/Z
/comments?before=A&after=B

安全

如果我們共享資源鏈接,則 URL 內編碼的所有數據都可能會暴露給第三方,即使他們無權從我們的 API 請求表示。

當在互聯網上通過 HTTP 請求任何內容時,中間件都會記錄 URL,因此甚至不必在社交媒體或類似媒體上主動分享鏈接。

例如此圖片鏈接:

/users/:userName/images/:imageId

如果我們將其分享到某個地方,我們就會知道我們有一個具有特定名稱的用戶,并且他們在我們的服務上上傳了圖像。

如果圖像鏈接是根資源,則不會出現此類信息。

/images/:imageId

更改 URL

如果我們的關系發生變化,它們編碼的 URL 就不再穩定。

有時這可能很有用,但更多的時候我們希望保留我們的 URL,以便舊鏈接不會停止工作。

例如,這種所有者-產品關系:

/owners/kay/products/1234
/owners/xing/products/1234

如果該產品可以作為根資源訪問,那么誰擁有它就無關緊要了。

/products/1234

正如我之前提到的,如果關系經常發生變化,我們也可以考慮將關系本身視為一種資源。

/posessions?owner=kay&product=1234

通過這種方法,我們可以通過一個端點改變關系,但通過不受此改變影響的自己的根資源直接鏈接我們的其他資源。

總結

那么這一切的啟示是什么呢?

我們是否應該嵌套我們的資源?

有時這是無法避免的,因為數據源根本沒有給我們任何其他選擇,但如果我們有選擇,我們應該考慮所有的利弊。

如果數據是嚴格分層的,嵌套不是太深,并且關系不會經常更改,我會使用嵌套資源。

與開發人員體驗方面的優勢相比,這些缺點并不算太大。

如果數據容易發生關系變化或者一開始就有相當復雜的關系,那么維護根資源甚至考慮完全不同的方法(如 GraphQL)會更容易。

更多的端點,以及嵌套場景所暗示的更復雜的端點意味著需要編寫更多的代碼和文檔。這不會引起技能或專業知識方面的可行性問題,而往往只是開發和維護成本的問題。因此,即使我們知道如何做,并且安全性或可緩存性不是問題,我們也必須問自己這是否能給我們帶來任何競爭優勢。

原文鏈接:REST API Design Best Practices for Sub and Nested Resources

上一篇:

REST API 設計參數和查詢字符串使用的最佳實踐

下一篇:

精通API規范:構建明確指導和預期的指南
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費