RESTful API路由设计:嵌套与非嵌套的区别

29

我的问题是在构建API URL时,嵌套资源的优势。考虑以下两种访问员工资源的替代方案:

/api/employees?department=1   # flat

Vs.

/api/departments/1/employees  # nested

现在考虑开发一个通用库,用于访问 API 中的 REST 资源。如果所有路由都是扁平化的,这样的 REST 封装库只需要知道所访问资源的名称:

store.query('employees', {department_id:1})   =>   /api/employees?department=1
然而,如果我们要支持嵌套路由,这个包装器就需要知道额外的信息,例如哪些模型是嵌套的以及它们属于哪个其他资源,以便知道如何构建引用此类模型的URL。鉴于并非所有模型都会嵌套在同一个父资源下,甚至有些模型根本不会嵌套,因此REST包装库需要具有描述所有这些额外知识的配置,否则就不需要这些知识。
所以我的问题是:
  • 在API中使用嵌套资源路由有什么真正的优势?(它并不适用于最终用户,并且从拥有更漂亮的URL中获得的好处更少)。

  • 嵌套方法是否真的比平面结构更好,除了美学之外,是否值得引入支持资源URL构建的不一致所带来的额外工作和复杂性?

参见:https://dev59.com/qGEi5IYBdhLWcg3wx-xs#36410780 更新:重要澄清 我意识到从一些评论和答案中,对于一个方面我没有表述清楚:我并不反对使用像/employees/5/departments/1这样的URL来寻址单个资源。我不认为那是嵌套的。
当我说嵌套资源时,我指的是像/departments/1/employees这样的URL,其中一个资源始终在另一个资源的上下文中被寻址。主要问题在于事实上,对于URL构建,通用库需要知道额外的信息,例如“员工嵌套在部门下”,但“分支不嵌套在任何东西下”。如果所有资源都可以以RESTful但是平面的方式来寻址,则更简单且更可预测地了解如何寻址它们。
当您考虑它时,在数据库中,您无需了解额外信息即可知道如何寻址一组对象(例如关系数据库管理系统中的表)。您总是将员工集合称为employees,而不是departments/5/employees

我认为REST API是访问资源的一种方式,而且访问资源的方式应该尽可能简单。根据你的应用程序选择更合适的选项。我更喜欢你列出的第一个选项,因为它最有意义(也最常见)。我会使用嵌套的REST API URL来深入挖掘资源(例如/employees/{uid}或/departments/{uid})。部门和员工之间似乎存在双向关系,也许还可以有一个API公开dpts->employee获取。例如/departments?user.name=Ernesto。当然,这完全取决于您如何构建后端。 - Lucas Crawford
Lucas Crawford:请参见我上面关于像/employees/{uid}这样的URL的澄清。我不关心那些,因为它们不属于嵌套资源的定义范畴。 - Ernesto
1
一个URI作为一个整体是指向某些资源的指针,包括任何路径、矩阵或查询参数。你可以把URI看作是(intermediary)缓存用来确定是否有可用于该键的表示的关键字。单独地,URI并不传达任何父子关系。因此,像/api/companies/123/users/456这样的URI并不一定说明用户是公司的子资源。你可以设计你的系统来确切地做到这一点,但客户端不应该依赖这样的知识!相反,使用链接关系来提示客户端有关语义上下文的信息。 - Roman Vottner
4个回答

6

如果您想要进一步深入了解,会发生什么情况呢?

/api/addresses?departmentId=1&employeeId=2&addressId=3

相对于

/api/departments/1/employees/2/addresses/3

地址终端突然变得臃肿不堪。

此外,如果您正在查看Richardson成熟度模型的第3级,RESTful API应该可以通过链接进行发现。例如,在顶层,比如/api/version(/1),您可以发现有一个链接到部门的链接。以下是类似HAL Browser这样的工具的示例:

"api:department-query": {
  "href": "http://apiname:port/api/departments?pageNumber={pageNumber}&pageSize={pageSize}&sort={sort}"
},
"api:department-by-id": {
  "href": "http://apiname:port/api/departments?departmentId={departmentId}"
}

这里需要提供一个查询,以分页的方式列出所有部门,或者提供一个参数化链接,可以直接跳转到特定的部门,前提是您知道id。

优点在于客户端只需要知道关系(链接)名称,而服务器大多数情况下可以自由更改关系(和资源)的url。


7
感谢您的回复。跟进一下:“扁平”版本“参数过多”到底有什么问题?除了使用“参数过多”一词外,它具有查询字符串参数的事实是不好的,因为...?最终,我认为所有反对意见都归结为“扁平方法会导致更丑陋的URL”。我正在努力弄清楚美学之外是否还有其他问题。 - Ernesto
4
顺便提一下,我认为即使支持嵌套方法的人也会警告不要像你所示例的那样进行过深的嵌套。是这样吗? - Ernesto
这不仅仅是关于更丑陋的URL,这并不是很重要,而是关于你代码内部的内容。同时,它也涉及到为客户端公开所需的用例,恰到好处。第一个场景也更像是"SOAP" - 只需使用y参数调用x方法(也称为“departments”/“addresses”),具体取决于您想要什么(整个部门的地址?那么employeeId就是可选的)。 - Alexandru Marculescu
7
阅读了您的回答后,我更加详细地理解了您提到的例子/api/departments/1/employees/2/addresses/3。在我的扁平结构中,这个例子实际上应该是/addresses/3,而不是像您所建议的/api/addresses?departmentId=1&employeeId=2&addressId=3。这正是我的观点所在,即您不需要额外的ID来寻址嵌套资源。 - Ernesto
1
嵌套资源的最大缺点之一是,如果父资源的id不正确或不匹配,可能会导致返回不正确的数据。假设没有授权问题,这取决于api实现来验证传递的嵌套资源确实是父资源的子资源。如果没有编码进行此检查,则api响应可能不正确,导致数据损坏。 - Andy Dufresne
显示剩余2条评论

5

这篇旧文章对我来说并不令人满意。

这取决于你的API。如果你的数据是分层的,并且不需要访问没有通过其父级过滤的资源,那么嵌套式设计是可以的(非嵌套式也可以)。

如果你的ID很长(GUID),你的层次结构很深,或者你需要访问任何资源而不需要通过其父代进行过滤,则非嵌套式是一个不错的选择。

尽量拥有统一的接口,不要为访问同一资源提供多种方式。

请尝试此链接,它可以更好地解释: https://www.moesif.com/blog/technical/api-design/REST-API-Design-Best-Practices-for-Sub-and-Nested-Resources/


说得好。正确的答案应该是,“这取决于你的设计”。 - undefined

0

根据我的经验: Q1. 使用起来更容易,但由于关系模型的原因,实现起来更困难。 Q2. 当涉及到权限和其他潜在检查时,嵌套更好,这样你可以在进入更深层之前进行检查。


1
感谢您的反馈。您能详细说明一下,在嵌套版本中,有哪些检查是您无法在扁平化方法中进行的吗? - Ernesto

0

基于模型和安全性,我会选择第二种解决方案进行投票。

该部门在路径中,不需要出现在有效负载中,无论是读还是写。

如果要更改员工的部门,则可以将depID包含在有效负载中或通过单独的端点(具有单独的授权)/employees/{ID}进行更改。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接