我应该为REST资源使用单数还是复数名称约定?

我是REST新手,我观察到在一些RESTful服务中,它们使用不同的资源URI进行更新/获取/删除和创建。例如

  • 创建-在某些地方使用/资源(单数)使用资源和POST方法(观察复数)
  • 更新-使用/资源/123和PUT方法
  • Get-使用/资源/123和GET方法

我对这个URI命名约定有点困惑。我们应该用复数还是单数来创建资源?在决定资源创建时应该用什么标准?

285258 次浏览

使用/resources的前提是它代表“所有”资源。如果您执行GET /resources,您可能会返回整个集合。通过发布到/resources,您正在添加到集合中。

但是,单个资源在 /resource.如果您执行GET /resource,则可能会出错,因为此请求没有任何意义,而/resource/123则完全有意义。

使用/resource而不是/resources类似于如果您使用文件系统和文件集合,而/resource是其中包含单个123456文件的“目录”。

不管是对是错,选择你最喜欢的。

我也不明白这样做的意义,我认为这不是最好的URI设计。作为RESTful服务的用户,无论我是访问列表还是访问列表中的特定资源,我都希望列表资源具有相同的名称。无论您是要使用列表资源还是特定资源,您都应该使用相同的标识符。

尽管最流行的实践是RESTful apis,其中使用复数,例如/api/resources/123,但有一种特殊情况,我发现使用单数名称比复数名称更合适/更具表现力。这是一对一关系的情况。特别是如果目标项是一个值对象(在领域驱动的设计范式中)。

让我们假设每个资源都有一个一对一的accessLog,它可以建模为值对象,即不是实体,因此没有ID。它可以表示为/api/resources/123/accessLog。通常的动词(POST、PUT、DELETE、GET)将适当地表达意图以及关系确实是一对一的事实。

为什么不遵循数据库表名的流行趋势,其中通常接受单一形式?去过那里,做到了——让我们重用。

表命名困境:单数名称与复数名称

对我来说,最好有一个可以直接映射到代码的模式(易于自动化),主要是因为代码是两端的内容。

GET  /orders          <---> orders
POST /orders          <---> orders.push(data)
GET  /orders/1        <---> orders[1]
PUT  /orders/1        <---> orders[1] = data
GET  /orders/1/lines  <---> orders[1].lines
POST /orders/1/lines  <---> orders[1].lines.push(data)

从API消费者的角度来看,端点应该是可预测的

理想情况下…

  1. GET /resources应该返回一个资源列表。
  2. GET /resource应该返回400级状态代码。
  3. GET /resources/id/{resourceId}应该返回一个包含一个资源的集合。
  4. GET /resource/id/{resourceId}应该返回一个资源对象。
  5. POST /resources应该批量创建资源。
  6. POST /resource应该创建一个资源。
  7. PUT /resource应该更新资源对象。
  8. PATCH /resource应该通过仅发布更改的属性来更新资源。
  9. PATCH /resources应该批量更新资源,只发布更改的属性。
  10. DELETE /resources应该删除所有资源;开玩笑的:400状态码
  11. DELETE /resource/id/{resourceId}

这种方法是最灵活、功能最丰富的,但也是最耗时的开发方法。所以,如果你很着急(软件开发总是这样),只需将你的端点命名为resource或复数形式resources。我更喜欢单数形式,因为它让你可以选择以编程方式自省和评估,因为并非所有复数形式都以's'结尾。

话虽如此,无论出于何种原因,最常用的实践开发人员选择的是使用复数形式。这最终是我选择的路线,如果你看看像githubtwitter这样流行的api,这就是他们所做的。

决定的一些标准可以是:

  1. 我的时间限制是什么?
  2. 我将允许我的消费者进行哪些操作?
  3. 请求和结果负载是什么样子的?
  4. 我是否希望能够在代码中使用反射并解析URI?

所以这取决于你。无论你做什么都要始终如一。

我的意见是:那些花时间从复数变为单数或从复数变为单数的方法是对CPU周期的浪费。我可能是老派的,但在我的时代,类似的东西被称为相同的。我如何查找与人有关的方法?没有一个常规的表达会同时覆盖人和人而没有不良副作用。

英语复数可能非常武断,它们不必要地阻碍了代码。坚持一个命名惯例。计算机语言应该是关于数学清晰度的,而不是模仿自然语言。

复数

  • 简单-所有URL都以相同的前缀开头
  • 逻辑-orders/获取订单的索引列表。
  • 标准-最广泛采用的标准,其次是绝大多数公共和私有API。

例如:

GET /resources-返回资源项列表

POST /resources-创建一个或多个资源项

PUT /resources-更新一个或多个资源项

PATCH /resources-部分更新一个或多个资源项

DELETE /resources-删除所有资源项

对于单个资源项目:

GET /resources/:id-返回基于:id参数的特定资源项

POST /resources/:id-创建一个具有指定id的资源项(需要验证)

PUT /resources/:id-更新特定的资源项

PATCH /resources/:id-部分更新特定资源项

DELETE /resources/:id-删除特定的资源项

对于单数的倡导者,可以这样想:你会向某人请求order并期望一件事或一系列事情吗?那么,当你键入/order时,为什么你会期望服务返回一系列事情呢?

单数

<强>便利 事物可以有不规则的复数名称。有时它们没有。 但是单数名称总是存在的。

例如客户地址覆盖客户地址

考虑这个相关的资源。

这个/order/12/orderdetail/12/orders/12/orderdetails/4更具可读性和逻辑性。

数据库表格

资源表示类似数据库表的实体。 它应该有一个逻辑单数名称。 下面是回答 over表名。

类映射

类总是单数的。ORM工具生成与类名同名的表。随着越来越多的工具被使用,单数名称正在成为一种标准。

阅读更多关于REST API开发人员的困境

对于没有单数名称的事物

trouserssunglasses的例子中,它们似乎没有一个单一的对应物。它们是众所周知的,并且它们在使用时看起来是单数的。就像一双鞋子。考虑命名类文件ShoeShoes。在这里,这些名称必须通过它们的使用被认为是一个单数实体。你不会看到有人买一只鞋来将URL作为

/shoe/23

我们必须将Shoes视为一个单一的实体。

参考:6大REST命名最佳实践

使用命名约定,通常可以安全地说“只是选择一个并坚持下去”,这是有道理的。

然而,在不得不向很多人解释REST之后,将端点表示为文件系统上的路径是最具表现力的方式。
它是无状态的(文件存在或不存在),分层,简单且熟悉-您已经知道如何访问静态文件,无论是本地还是通过超文本传输协议。

在这种情况下,语言规则只能让你达到以下几点:

一个目录可以包含多个文件和/或子目录,因此它的名称应该是复数形式。

我喜欢这样
虽然,另一方面-它是您的目录,但如果您想要的话,您可以将其命名为“a-resource-or-multial-Resources”。这不是真正重要的事情。

重要的是,如果您将名为“123”的文件放在名为“resourceS”的目录下(导致/resourceS/123),则不能期望通过/resource/123访问它。

不要试图让它变得更聪明——根据你当前访问的资源数量从复数变为单数可能会让一些人感到美观,但它并不有效,在等级系统中也没有意义。

注意:从技术上讲,您可以制作“符号链接”,这样/resources/123也可以通过/resource/123访问,但前者仍然必须存在!

对所有方法使用复数至少在一个方面更实用: 如果您正在使用Postman(或类似工具)开发和测试资源API,则在从GET切换到PUT到POST等时不需要编辑URI。

为了简单和一致,我更喜欢使用单数形式。

例如,考虑以下URL:

/customer/1

我将客户视为客户集合,但为了简单起见,集合部分被删除。

另一个例子:

/equipment/1

在这种情况下,设备不是正确的复数形式。因此,将其视为设备集合并为简单起见删除集合使其与客户案例一致。

对我来说,复数操作收藏,而单数操作该集合中的项目

收藏允许方法获取/发布/删除

项目允许方法获取/删除

例如

/students上的帖子将在学校增加一名新学生。

删除/students将删除学校中的所有学生。

删除/student/123将从学校删除学生123。

这可能感觉不重要,但一些工程师有时会忘记id。如果路由始终是复数并执行DELETE,您可能会不小心擦除数据。而缺少单数上的id将返回未找到的404路由。

为了进一步扩展示例,如果API应该公开多个学校,则类似

删除/school/abc/students将删除学校abc中的所有学生。

有时选择正确的单词本身就是一个挑战,但我喜欢保持集合的多样性。例如。cart_itemscart/items感觉是对的。相比之下,删除cart,删除的是购物车对象本身,而不是购物车中的项目;)。

我很惊讶地看到这么多人会跳上复数名词的潮流。在实现单复数转换时,你是否在照顾不规则的复数名词?你享受痛苦吗?

http://web2.uvcs.uvic.ca/elc/studyzone/330/grammar/irrplu.htm

有许多类型的不规则复数,但这些是最常见的:

名词类型构成复数例

Ends with -fe   Change f to v then Add -s
knife   knives
life   lives
wife   wives
Ends with -f    Change f to v then Add -es
half   halves
wolf   wolves
loaf   loaves
Ends with -o    Add -es
potato   potatoes
tomato   tomatoes
volcano   volcanoes
Ends with -us   Change -us to -i
cactus   cacti
nucleus   nuclei
focus   foci
Ends with -is   Change -is to -es
analysis   analyses
crisis   crises
thesis   theses
Ends with -on   Change -on to -a
phenomenon   phenomena
criterion   criteria
ALL KINDS   Change the vowel or Change the word or Add a different ending
man   men
foot   feet
child   children
person   people
tooth   teeth
mouse   mice
Unchanging Singular and plural are the same
sheep deer fish (sometimes)

两种表示都很有用。为了方便起见,我使用单数已经有一段时间了,拐点可能很困难。根据我开发严格单数REST API的经验,使用端点的开发人员对结果的形状缺乏确定性。我现在更喜欢使用最能描述响应形状的术语。

如果你所有的资源都是顶级的,那么你就可以逃脱单一的表示。避免拐点是一个巨大的胜利。

如果您正在执行任何类型的深度链接来表示对关系的查询,那么针对您的API编写的开发人员可以通过更严格的约定来得到帮助。

我的惯例是URI中的每个深度级别都描述与父资源的交互,完整的URI应该隐式描述正在检索的内容。

假设我们有以下模型。

interface User {
<string>id;
<Friend[]>friends;
<Manager>user;
}


interface Friend {
<string>id;
<User>user;
...<<friendship specific props>>
}

如果我需要提供一个资源,允许客户端获取特定用户的特定朋友的经理,它可能看起来像:

GET /users/{id}/friends/{friendId}/manager

以下是一些更多的例子:

  • GET /users-列出全局用户集合中的用户资源
  • POST /users-在全局用户集合中创建一个新用户
  • GET /users/{id}-从全局用户集合中检索特定用户
  • GET /users/{id}/manager-获取特定用户的经理
  • GET /users/{id}/friends-获取用户的好友列表
  • GET /users/{id}/friends/{friendId}-获取用户的特定好友
  • LINK /users/{id}/friends-向该用户添加好友关联
  • UNLINK /users/{id}/friends-从此用户中删除好友关联

请注意每个级别如何映射到可以操作的父级。对同一对象使用不同的父级是违反直觉的。在GET /resource/123检索资源不会表明应该在POST /resources创建新资源

我更喜欢使用复数(/resources)和单数(/resource/{id}),因为我认为它更清楚地区分了处理资源集合和处理单个资源之间的逻辑。

作为一个重要的副作用,它还有助于防止有人错误使用API。例如,考虑用户错误地尝试通过将Id指定为参数来获取资源的情况,如下所示:

GET /resources?Id=123

在这种情况下,我们使用复数版本,服务器很可能会忽略Id参数并返回所有资源的列表。如果用户不小心,他会认为调用成功并使用列表中的第一个资源。

另一方面,当使用单数形式时:

GET /resource?Id=123

服务器很可能会返回错误,因为Id没有以正确的方式指定,用户必须意识到有问题。

我知道大多数人都在决定是使用复数还是单数之间。这里没有解决的问题是客户需要知道你使用的是哪一个,他们总是可能出错。这就是我的建议的来源。

两个都要怎么样?我的意思是对整个API使用单数,然后创建路由将以复数形式发出的请求转发到单数形式。例如:

GET  /resources     =     GET  /resource
GET  /resources/1   =     GET  /resource/1
POST /resources/1   =     POST /resource/1
...

你得到图片。没有人是错的,最小的努力,和客户将永远得到它的权利。

不如:

/resource/(不是/resource

/resource/意味着它是一个包含称为“资源”的文件夹,它是一个“resouce”文件夹。

而且我认为数据库表的命名约定是相同的,例如,一个名为“user”的表是一个“user table”,它包含一个名为“user”的东西。

路由中的id应与列表的索引相同,并且应相应地进行命名。

numbers = [1, 2, 3]


numbers            GET /numbers
numbers[1]         GET /numbers/1
numbers.push(4)    POST /numbers
numbers[1] = 23    PUT /numbers/1

但是有些资源在其路由中不使用id,因为只有一个,或者用户永远无法访问多个,所以这些不是列表:

GET /dashboard
DELETE /session
POST /session
GET /users/{:id}/profile
PUT /users/{:id}/profile

请参阅Google的API设计指南:资源名称以了解命名资源的另一种方式。

指南要求集合以复数命名。

|--------------------------+---------------+-------------------+---------------+--------------|
| API Service Name         | Collection ID | Resource ID       | Collection ID | Resource ID  |
|--------------------------+---------------+-------------------+---------------+--------------|
| //mail.googleapis.com    | /users        | /name@example.com | /settings     | /customFrom  |
| //storage.googleapis.com | /buckets      | /bucket-id        | /objects      | /object-id   |
|--------------------------+---------------+-------------------+---------------+--------------|

值得一读,如果你正在思考这个问题。

我不喜欢看到URL的{id}部分与子资源重叠,因为id理论上可以是任何东西,并且会有歧义。它混合了不同的概念(标识符和子资源名称)。

类似的问题经常出现在enum常量或文件夹结构中,其中混合了不同的概念(例如,当您有文件夹TigersLionsCheetahs时,然后还有一个名为Animals的文件夹在同一级别-这是没有意义的,因为一个是另一个的子集)。

一般来说,我认为如果端点一次处理单个实体,则端点的最后命名部分应该是单数,如果它处理实体列表,则应该是复数。

因此,处理单个用户的端点:

GET  /user             -> Not allowed, 400
GET  /user/{id}        -> Returns user with given id
POST /user             -> Creates a new user
PUT  /user/{id}        -> Updates user with given id
DELETE /user/{id}      -> Deletes user with given id

然后有单独的资源用于对用户进行查询,通常返回一个列表:

GET /users             -> Lists all users, optionally filtered by way of parameters
GET /users/new?since=x -> Gets all users that are new since a specific time
GET /users/top?max=x   -> Gets top X active users

这里有一些处理特定用户的子资源的示例:

GET /user/{id}/friends -> Returns a list of friends of given user

交朋友(多对多链接):

PUT /user/{id}/friend/{id}     -> Befriends two users
DELETE /user/{id}/friend/{id}  -> Unfriends two users
GET /user/{id}/friend/{id}     -> Gets status of friendship between two users

永远不会有任何歧义,资源的复数或单数命名是对用户的暗示,他们可以期待什么(列表或对象)。对#EYZ没有限制,理论上可以拥有idnew的用户,而不会与(潜在的未来)子资源名称重叠。

使用单数并利用例如“Business Directory”中的英语约定。

很多东西都是这样读的:“书箱”,“狗包”,“美术馆”,“电影节”,“停车场”等。

这方便地从左到右匹配url路径。左侧的项目类型。右侧的设置类型。

GET /users真的获取了一组用户吗?通常不会。它获取的是一组包含键和用户名的存根。所以它也不是真正的/users。它是一个用户索引,或者你愿意的话称为“用户索引”。为什么不这么称呼它呢?它是一个/user/index。由于我们已经命名了集合类型,我们可以有多种类型来显示用户的不同投影,而无需求助于查询参数,例如user/phone-list/user/mailing-list

那么用户300呢?它仍然是/user/300

GET /user/index
GET /user/{id}


POST /user
PUT /user/{id}


DELETE /user/{id}

最后,HTTP对单个请求只能有一个响应。路径总是指一个单一的东西。

最重要的事情

每当你在接口和代码中使用复数时,问问自己,你的约定如何处理这样的单词:

  • /pants/eye-glasses-是单数路径还是复数路径?

  • /radii-你知道你的头顶,如果单数路径是/radius/radix

  • /index-你知道你的头顶,如果复数路径是/indexes/indeces/indices

理想情况下,约定应该在没有不规则的情况下扩展。英语复数不这样做,因为

  1. 它们有例外,比如某个东西被复数形式调用,并且
  2. 没有简单的算法可以从单数中获取单词的复数,从复数中获取单数,或者判断未知名词是单数还是复数。

这有缺点。我脑海中最突出的是:

  1. 单数和复数形式相同的名词将迫使您的代码处理“复数”端点和“单数”端点无论如何都具有相同路径的情况。
  2. 您的用户/开发人员必须精通英语,才能知道名词的正确单数和复数。在日益国际化的世界中,这会造成不可忽视的挫败感和开销。
  3. 它单枪匹马地将“我知道/foo/\{\{id}},获取所有foo的路径是什么?”变成了一个自然语言问题,而不是“只需删除最后一个路径部分”问题。

与此同时,一些人类语言甚至没有不同的名词单复数形式。它们管理得很好。你的API也是如此。

这是Roy Fielding的论文“架构风格与基于网络的软件架构设计”,这句话可能会引起你的兴趣:

资源是一个概念映射 到一组实体,而不是对应于中任何特定点的映射的实体 时间

作为一个资源,映射到一组实体,在我看来,使用/product/作为访问一组产品的资源,而不是/products/本身,似乎是不合逻辑的。如果你需要一个特定的产品,那么你可以访问/products/1/

作为进一步的参考,这个来源有一些关于资源命名约定的单词和示例:

命名约定或者说没有建立本地标准,根据我的经验,这是许多长时间值班的根本原因,头痛,风险重构,狡猾的部署,代码审查辩论,等等,等等。特别是当它决定改变需要的时候,因为一开始没有充分考虑。

一个实际问题跟踪了对此的讨论:

https://github.com/kubernetes/kubernetes/issues/18622

看到这方面的分歧很有趣。

我的观点是(加上一点令人头痛的经历),当你考虑像用户、帖子、订单、文档等常见实体时,你应该总是将它们作为实际实体来处理,因为这是数据模型的基础。语法和模型实体不应该在这里混淆,这会导致其他方面的混淆。然而,一切都是非黑即白的吗?很少是这样。上下文真的很重要。

当您希望在系统中获取用户集合时,例如:

GET/user->实体用户集合

GET/user/1->实体资源User: 1

说我想要一个实体用户的集合和说我想要用户的集合都是有效的。

GET/users->实体用户集合

GET/users/1->实体资源User: 1

从这个你说,从用户的集合,给我用户/1

但是如果你分解用户的集合是什么……它是一个实体的集合,其中每个实体都是User实体。

您不会说实体是Users,因为单个数据库表通常是User的单个记录。然而,我们这里谈论的是RESTful服务而不是数据库ERM。

但这只适用于具有明确名词区分的用户,并且很容易理解。然而,当您在一个系统中有多种相互冲突的方法时,事情会变得非常复杂。

事实上,无论哪种方法在大多数时候都是有意义的,除了少数情况下,英语只是意大利面条。它似乎是一种迫使我们做出许多决定的语言!

简单的事实是,无论你决定什么,在你的意图中保持一致和逻辑

在我看来,这里和那里的混合是一种糟糕的方法!这悄悄地引入了一些完全可以避免的语义歧义。

看似独特的偏好:

https://www.haproxy.com/blog/using-haproxy-as-an-api-gateway-part-1/

类似的讨论在这里:

https://softwareengineering.stackexchange.com/questions/245202/what-is-the-argument-for-singular-nouns-in-restful-api-resource-naming

这里的首要常数是,它确实似乎取决于某种程度的团队/公司文化偏好,根据更大公司指南中的细节,这两种方式都有许多优点和缺点。谷歌不一定是正确的,只是因为它是谷歌!这适用于任何指南。

避免把你的头埋在沙子里太多,松散地建立你对轶事例子和观点的整个理解系统。

你是否必须为所有事情建立坚实的推理。如果它适合你,或者你的团队和/我们的客户,并且对新的和经验丰富的开发人员有意义(如果你在团队环境中),那就太好了。

只是保持一致

使用单数:

POST /resource
PUT  /resource/123
GET  /resource/123

或复数:

POST /resources
PUT  /resources/123
GET  /resources/123