所有文章 > API设计 > 子资源和嵌套资源的 REST API 设计最佳实践
子资源和嵌套资源的 REST API 设计最佳实践

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

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

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

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

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

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

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

优点

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

为什么要采用这种方法:

/posts/:postId/comments/:commentId
/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
  • 有没有 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

安全

如果我们共享资源链接,则 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

#你可能也喜欢这些API文章!