子资源和嵌套资源的 REST API 设计最佳实践

2025-06-09

子资源和嵌套资源的 REST API 设计最佳实践

封面图片由 Marco Verch(专业摄影师和演讲者)在 Flickr 上提供

当我们开始设计 API 时,会出现许多问题,特别是当我们想要创建 REST API 并遵守REST 核心原则时

  • 客户端-服务器架构
  • 无国籍
  • 可缓存性
  • 分层系统
  • 统一接口

这个领域中经常被争论的一个话题是资源嵌套,也称为子资源

  • 为什么有人会嵌套他们的资源?
  • 首先,它们是一个好主意吗?
  • 我们应该嵌套我们的资源吗?
  • 我们应该何时嵌套我们的资源?
  • 如果我们嵌套资源,应该注意什么?

由于这个决定会对 API 的许多部分产生相当大的影响,例如安全性、可维护性或可更改性,因此我想对这个主题进行一些阐述,希望它有助于使这个决定更加明智。

首先,我们将探讨嵌套资源存在的原因。之后,我们将讨论导致嵌套资源出现问题的原因。

为什么

让我们从核心问题开始:为什么要使用嵌套资源设计方法?

为什么要采用这种方法:

/posts/:postId/comments/:commentId
/users/:userName/articles/:articleId
Enter fullscreen mode Exit fullscreen mode

关于这个:

/comments/:commentId
/articles/:articleId
Enter fullscreen mode Exit fullscreen mode

采用这种方法的主要原因是为了提高可读性;嵌套的资源 URL 可以传达一个资源属于另一个资源的信息。它呈现出一种层级关系,就像文件系统中的目录一样。

这些 URL 传达的关系含义较少:

/books/:bookId
/rating/:ratingId
Enter fullscreen mode Exit fullscreen mode

比这些 URL:

/books/:bookId
/books/:bookId/ratings/:ratingId
Enter fullscreen mode Exit fullscreen mode

我们可以直接看到请求的评分属于哪本书。在很多情况下,这可以使调试更容易。

之所以说是层级关系,是因为底层数据模型并非一定是层级化的。例如,在 GitHub 上,一个用户可以向多个仓库贡献代码,而一个仓库也可以包含来自不同用户的贡献。这是一种多对多的关系。

/users/:userName/repos
/repos/:repoName/users
Enter fullscreen mode Exit fullscreen mode

如果您只知道其中一个端点,那么它看起来就像是一对多的关系。

其他更具技术性的原因是嵌套资源的相对 ID上下文。

例如,房屋有门牌号,但这些门牌号只对应其所属的街道。如果你知道房子的门牌号是42,但却不记得街道名称,那么这对你来说就没什么帮助。

/street/:streetName/house/:houseNumber
Enter fullscreen mode Exit fullscreen mode

另一个例子是文件系统中的文件名。README.md如果数百个不同的目录中有数百个同名文件,仅仅知道我们的文件叫什么名字是没用的。

/home/kay/Development/project-a/README.md
/home/kay/Development/project-b/README.md
Enter fullscreen mode Exit fullscreen mode

如果我们使用关系数据库,我们通常对所有数据记录都有唯一的键,但正如我们所见,对于其他类型的数据存储(如文件系统),情况不一定如此。

嵌套 URL 的操作也相当简单。如果 URL 中编码了层级结构,我们可以删除 URL 的某些部分,从而向上层级推进。这使得包含嵌套资源的 API 导航变得相当简单。

总而言之,我们希望使用嵌套资源来提高可读性,进而提高开发人员的体验,有时我们甚至必须使用它们,因为数据源不提供仅通过其 ID 来识别嵌套资源的方法

为什么不

现在我们已经讨论了为什么我们应该使用嵌套,那么讨论另一方面也很重要:为什么我们不应该嵌套我们的资源?

虽然筑巢有时是必要的并且无法避免,但它通常是一个我们应该牢记的特定成本或危险的选择。

让我们逐一看一下。

可能很长的 URL

我们之前了解到嵌套资源可以使我们的 URL 更具可读性,但这并非万无一失。

特别是在资源之间存在许多关系的相当复杂的系统中,嵌套方法可能会导致相当长且复杂的 URL。

/customers/:customerId/projects/:projectId/orders/:orderId/lines/:lineId
Enter fullscreen mode Exit fullscreen mode

如果我们使用长字符串作为 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
Enter fullscreen mode Exit fullscreen mode

因此,当我们开始走这条路时,我们应该有时退后一步,看看我们是否仍然能够实现提高可读性的目标。

经验法则是最大嵌套深度为 2。有时深度为 3 也是可以的。例如,如果我们的 ID 很短且易于阅读。

/author/kay-ploesser/book/react-from-zero/review/23
Enter fullscreen mode Exit fullscreen mode

什么是 Moesif?Moesif是最先进的 API 分析服务,超过 2000 家组织使用它来了解您的客户如何使用您的 API 以及他们最常使用哪些资源。

冗余端点

一般来说,使用嵌套资源不如仅使用根资源那么灵活。

例如,如果我们有多对多关系,那么存储库有多个贡献者,但每个用户也可以为各种存储库做出贡献。

如果我们想通过嵌套资源实现这一点,我们必须为这种关系单独创建两个端点

/user/:userName/repositories
/repositories/:repositoryName/contributors
Enter fullscreen mode Exit fullscreen mode

如果我们想在不嵌套的情况下实现这一点,我们可以为贡献定义一个根资源,该资源还允许在其 URL 中使用过滤参数。

/contributions?userName=:userName&repositoryName=:repositoryName
Enter fullscreen mode Exit fullscreen mode

参数是可选的,所以我们也可以使用它来获取所有贡献,并且我们可以PUTPOST它来改变和创建关系。

虽然这似乎不是一对多关系的问题,其中关系的一部分不能有多个连接,但我们仍然可以到达想要在其父资源中搜索嵌套资源的所有记录的点。

因此,当有这个端点时:

/mothers/:motherName/children
Enter fullscreen mode Exit fullscreen mode

我们仍然可能希望获取所有母亲的所有孩子,并为此创建一个新的端点

/children
Enter fullscreen mode Exit fullscreen mode

冗余端点也会增加我们的 API 的表面,虽然资源关系中更易读的 URL 对开发人员体验来说是一件好事,但大量的端点却不是。

多个端点增加了 API 所有者记录整个过程的工作量,并使新客户的入职变得更加麻烦。

返回相同表示的多个端点也可能导致缓存问题,并可能违反RESTful API 设计的核心原则之一。

这个问题可以通过 HTTP 重定向来解决,因此所有表示都从中央根资源返回并可以缓存,但仍然需要代码来实现这一点。

它还可能违反另一个核心原则,即统一接口

当客户端持有资源的表示(包括附加的任何元数据)时,它就拥有足够的信息来修改或删除服务器上的资源,前提是它有权限这样做。

如果表示不包含有关嵌套的信息,并且我们没有根资源来直接访问它;我们就无法创建、更新或删除它。

多个数据库查询

如果我们向下遍历关系图而不是使用一个唯一标识符(如果存在)来从资源中检索表示,则我们需要检查 URL 中实现的关系是否成立。

以获取嵌套评论为例

/blogs/X/articles/Y/comments/Z
Enter fullscreen mode Exit fullscreen mode
  • 有没有 ID 为 X 的博客?
    • 我们去问问 DB 吧!
  • 我们的 ID 为 X 的博客是否有 ID 为 Y 的文章?
    • 我们去问问 DB 吧!
  • 我们的 ID Y 的文章是否有 ID Z 的评论?
    • 我们去问问 DB 吧!

获取所有博客所有文章的所有评论也是一个问题。

  1. 查询所有博客
  2. 查询每个博客的每篇文章
  3. 查询每篇文章的每条评论

此 API 设计给我们带来了严重的N+1 查询问题。

如果我们只有一个评论的根资源,我们可以查询它,并根据需要添加一些过滤参数。如果评论有全局唯一 ID,我们可以直接查询。

/comments/Z
/comments?before=A&after=B
Enter fullscreen mode Exit fullscreen mode

安全

如果我们共享资源链接,则 URL 内编码的所有数据都可能暴露给第三方,即使他们无权从我们的 API 请求表示。

当通过互联网上的 HTTP 请求任何内容时,中间件都会记录 URL,因此甚至不必在社交媒体或类似媒体上主动分享链接。

例如此图片链接:

/users/:userName/images/:imageId
Enter fullscreen mode Exit fullscreen mode

如果我们在某个地方分享它,我们就会知道我们有一个具有特定名称的用户,并且他们在我们的服务上上传了图像。

如果图像链接是根资源,则不会出现此类信息。

/images/:imageId
Enter fullscreen mode Exit fullscreen mode

更改 URL

如果我们的关系发生变化,它们编码的 URL 就不再稳定。

有时这可能很有用,但更多的时候我们希望保留我们的 URL,以便旧链接不会停止工作。

例如,这种所有者-产品关系:

/owners/kay/products/1234
/owners/xing/products/1234
Enter fullscreen mode Exit fullscreen mode

如果产品可以作为根资源访问,那么谁拥有它就无关紧要了。

/products/1234
Enter fullscreen mode Exit fullscreen mode

正如我之前提到的,如果关系变化比较频繁,我们也可以考虑将关系本身视为一种资源。

/posessions?owner=kay&product=1234
Enter fullscreen mode Exit fullscreen mode

通过这种方法,我们可以通过一个端点改变关系,但通过不受此变化影响的自己的根资源直接链接我们的其他资源。

包起来

那么这一切的结论是什么呢?

我们是否应该嵌套我们的资源?

有时这是无法避免的,因为数据源根本没有给我们任何其他选择,但如果我们有选择,我们就应该考虑所有的利弊。

如果数据是严格分层的,部署嵌套不太紧密,并且关系不会经常改变,那么我会使用嵌套资源。

与开发人员体验方面的优势相比,其缺点并不算太大。

如果数据容易发生关系变化或者一开始就有相当复杂的关系,则维护根资源会更容易,甚至可以考虑完全不同的方法,如 GraphQL。

更多的端点,以及嵌套场景所暗示的更复杂的端点,意味着需要编写更多的代码和文档。这通常不涉及技能或专业知识方面的可行性问题,而只是开发和维护成本的问题。因此,即使我们知道如何做到这一点,并且安全性或可缓存性并非主要考虑因素,我们也必须扪心自问,这是否会给我们带来任何竞争优势。


Moesif 是最先进的 API 分析平台,支持 REST、GraphQL 等多种语言。超过 2000 家组织使用 Moesif 来追踪其最忠实的客户如何使用他们的 API。 了解更多


最初发表于www.moesif.com

鏂囩珷鏉ユ簮锛�https://dev.to/moesif/rest-api-design-best-practices-for-sub-and-nested-resources-2m8a
PREV
每个 ASP.NET Core Web API 项目都需要什么 - 第 1 部分 - Serilog
NEXT
API 管理 vs API 网关,以及 API 分析和监控的适用场景?GenAI LIVE!| 2025 年 6 月 4 日