我是 REST 新手,我观察到在一些 RESTful 服务中,它们使用不同的资源 URI 进行更新/获取/删除和创建。如
创建 - 在某些地方使用 /resources 和 POST 方法(观察复数)使用 /resource(单数)
更新 - 使用 /resource/123 和 PUT 方法
获取 - 将 /resource/123 与 GET 方法一起使用
我对这个 URI 命名约定有点困惑。我们应该使用复数还是单数来创建资源?决定的标准应该是什么?
对我来说,最好有一个可以直接映射到代码的模式(易于自动化),主要是因为代码是两端的内容。
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)
使用 /resources
的前提是它代表“所有”资源。如果您执行 GET /resources
,您可能会返回整个集合。通过 POST 到 /resources
,您将添加到集合中。
但是,各个资源可在 /resource 中获得。如果您执行 GET /resource
,您可能会出错,因为此请求没有任何意义,而 /resource/123
则完全合理。
使用 /resource
而不是 /resources
与使用文件系统和文件集合的方法类似,/resource
是单个 123
的“目录”,{ 5} 个文件。
这两种方式都没有对错,选择你最喜欢的。
我也看不出这样做的意义,而且我认为这不是最好的 URI 设计。作为 RESTful 服务的用户,无论我访问列表还是列表中的特定资源,我都希望列表资源具有相同的名称。无论您要使用列表资源还是特定资源,都应该使用相同的标识符。
复数
简单 - 所有网址都以相同的前缀开头
逻辑 - 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
是处理引用一个订单的对象的单个实例的类的好名称。 OrderList
是处理多个 Order
实例的类的名称。 Orders Table
是一个包含许多订单的数据库表的好名字。
单数
方便事物可以有不规则的复数名称。有时他们没有。但是单数名称总是存在的。
例如 CustomerAddress 超过 CustomerAddresses
考虑这个相关的资源。
此 /order/12/orderdetail/12
比 /orders/12/orderdetails/4
更具可读性和逻辑性。
数据库表
资源代表一个实体,如数据库表。它应该有一个合乎逻辑的单数名称。这是表名称的 answer。
类映射
类总是单数的。 ORM 工具生成与类名同名的表。随着越来越多的工具被使用,单数名称正在成为一种标准。
详细了解A REST API Developer's Dilemma
对于没有单数名称的事物
在 trousers
和 sunglasses
的情况下,它们似乎没有单数对应物。它们是众所周知的,并且通过使用它们似乎是单数的。就像一双鞋。考虑将类文件命名为 Shoe
或 Shoes
。在这里,这些名称的使用必须被视为一个单一的实体。您不会看到有人购买单鞋以将 URL 设置为
/shoe/23
我们必须将 Shoes
视为一个单一的实体。
参考:Top 6 REST Naming Best Practices
/clothe/12/trouser/34
:)
clothe
这个词是一个动词。 Rest API 在谈论资源时通常使用名词,而在描述动作时使用动词。单数形式是 clout
,但它是古老的,可能更适合用 garment
代替。
尽管最普遍的做法是使用复数形式的 RESTful api,例如 /api/resources/123
,但在一种特殊情况下,我发现使用单数名称比复数名称更合适/更具表现力。这是一对一关系的情况。特别是如果目标项是一个值对象(在领域驱动设计范式中)。
让我们假设每个资源都有一个一对一的 accessLog
,它可以被建模为一个值对象,即不是一个实体,因此没有 ID。它可以表示为/api/resources/123/accessLog
。通常的动词(POST、PUT、DELETE、GET)会恰当地表达意图以及关系确实是一对一的事实。
GET /users/123/location
应获取用户工作的位置。 GET /users/123/locations
真的不会误导消费者吗?
accessLog
被建模为属性或值,而不是实体,因此它应该是单数的。如果您进行过度设计,那么日志条目将是一个实体,您将拥有 /api/accessLogEntries?resource=123
。
为什么不跟随普遍接受单数形式的数据库表名的流行趋势呢?去过那里,做到了——让我们重用。
Table Naming Dilemma: Singular vs. Plural Names
我很惊讶看到这么多人会加入复数名词的潮流。在实现单数到复数的转换时,您是否注意不规则的复数名词?你享受痛苦吗?
请参阅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)
从 API 使用者的角度来看,端点应该是可预测的,因此
理想情况下...
GET /resources 应该返回一个资源列表。 GET /resource 应该返回一个 400 级的状态码。 GET /resources/id/{resourceId} 应该返回一个包含一个资源的集合。 GET /resource/id/{resourceId} 应该返回一个资源对象。 POST /resources 应该批量创建资源。 POST /resource 应该创建一个资源。 PUT /resource 应该更新资源对象。 PATCH /resource 应该通过仅发布更改的属性来更新资源。 PATCH /resources 应该批量更新仅发布更改的属性的资源。 DELETE /resources 应该删除所有资源;开个玩笑:400 状态码 DELETE /resource/id/{resourceId}
这种方法最灵活、功能最丰富,但开发起来也最耗时。因此,如果您赶时间(软件开发总是如此),只需将端点命名为 resource
或复数形式 resources
。我更喜欢单数形式,因为它使您可以选择以编程方式自省和评估,因为并非所有复数形式都以“s”结尾。
说了这么多,无论出于何种原因,开发人员选择的最常用的做法是使用复数形式。这最终是我选择的路线,如果您查看像 github
和 twitter
这样的流行 api,它们就是这样做的。
一些决定标准可能是:
我的时间限制是什么?我将允许我的消费者执行哪些操作?请求和结果负载是什么样的?我是否希望能够在我的代码中使用反射和解析 URI?
所以这取决于你。无论你做什么都是一致的。
POST /users
应该创建一个用户,并将其添加到集合中。我不同意。 POST /users
应该创建一个用户列表(即使是 1 个列表),而 POST /user
应该只创建一个用户。我看不出为什么复数和单数资源端点不能共存。它们描述了不同的行为,不应让任何人对它们的功能感到惊讶。
POST users/<id>
将创建一个新用户。
PUT /users/<id>
而不是 POST
更为常见。 POST
的解释是“将其添加到集合中,并将 id 确定为其中的一部分”。 PUT
具有“使用此 ID 更新(或添加)此资源”的解释。有关此原理的详细说明,请参见 restcookbook.com/HTTP%20Methods/put-vs-post。
有关命名资源的另一种看法,请参阅 Google 的 API Design Guide: Resource Names。
该指南要求收藏品以复数形式命名。
|--------------------------+---------------+-------------------+---------------+--------------|
| 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 |
|--------------------------+---------------+-------------------+---------------+--------------|
如果您正在考虑这个主题,那么值得一读。
路由中的 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
我的两分钱:花时间从复数变为单数或反之亦然的方法是在浪费 CPU 周期。我可能是老派,但在我那个时代,事情被称为相同的东西。我如何查找有关人员的方法?没有一种常规的表达方式可以同时涵盖人和人而不会产生不良的副作用。
英语复数可能非常随意,它们不必要地妨碍代码。坚持一种命名约定。计算机语言应该是关于数学清晰度,而不是模仿自然语言。
为了简单和一致,我更喜欢使用单数形式。
例如,考虑以下网址:
/客户/1
我将客户视为客户收藏,但为简单起见,收藏部分被删除。
另一个例子:
/设备/1
在这种情况下,设备不是正确的复数形式。因此将其视为设备集合并为简单起见删除集合使其与客户案例一致。
POST /customer
不应该这样做 - 插入一个客户吗?
POST /customer
对我来说就像是在向 the
客户发送邮件。不是客户的集合。但是,我承认复数或非复数是一种偏好。只要它们不像其他答案那样混合。那将是令人难以置信的混乱。
GET child?q=""
没有什么不同。无论如何,您都需要防止意外多重,大多数端点应该有多重......使用单数不会改变这一点。对于 REST 原生 API 来说,复数形式似乎是标准。如果休息对您的应用程序来说是次要的,那么单数会更容易。
使用命名约定,通常可以安全地说“只选择一个并坚持下去”,这是有道理的。
然而,在不得不向很多人解释 REST 之后,将端点表示为文件系统上的路径是最具表现力的方式。它是无状态的(文件存在或不存在)、分层、简单且熟悉 - 您已经知道如何访问静态文件,无论是在本地还是通过 http。
在这种情况下,语言规则只能让你做到以下几点:
一个目录可以包含多个文件和/或子目录,因此它的名称应该是复数形式。
我喜欢这样。虽然另一方面 - 它是您的目录,但如果您想要的话,您可以将其命名为“a-resource-or-multiple-resources”。那不是真正重要的事情。
重要的是,如果您将名为“123”的文件放在名为“resourceS”的目录下(导致 /resourceS/123
),那么您不能期望它可以通过 /resource/123
访问。
不要试图让它变得比它必须的更聪明 - 根据您当前访问的资源数量从复数变为单数可能对某些人来说在美学上令人愉悦,但它不是有效的,而且它没有意义等级制度。
注意:从技术上讲,您可以制作“符号链接”,这样 /resources/123
也可以通过 /resource/123
访问,但前者仍然必须存在!
最重要的事情
每当你在接口和代码中使用复数时,问问自己,你的约定如何处理这样的词:
/pants, /eye-glasses - 这些是单数还是复数路径?
/radii - 你知道单数路径是 /radius 还是 /radix?
/index - 如果复数路径是 /indexes 或 /indeces 或 /indices,你知道吗?
理想情况下,约定应在没有不规则性的情况下扩展。英语复数不这样做,因为
它们有例外,例如以复数形式调用的东西之一,并且没有简单的算法可以从单数中获取单词的复数,从复数中获取单数,或者判断未知名词是单数还是复数。
这有缺点。我脑海中最突出的:
单数和复数形式相同的名词将强制您的代码处理“复数”端点和“单数”端点具有相同路径的情况。您的用户/开发人员必须精通英语,才能知道名词的正确单数和复数。在一个日益国际化的世界中,这可能会导致不可忽视的挫败感和开销。它单独变成“我知道 /foo/{{id}},获取所有 foo 的路径是什么?”进入自然语言问题,而不是“只删除最后一条路径”问题。
同时,一些人类语言甚至没有名词的单复数形式。他们管理得很好。你的 API 也可以。
我不希望看到 URL 的 {id}
部分与子资源重叠,因为 id
理论上可以是任何东西并且会有歧义。它混合了不同的概念(标识符和子资源名称)。
类似问题经常出现在 enum
常量或文件夹结构中,其中混合了不同的概念(例如,当您有文件夹 Tigers
、Lions
和 Cheetahs
,然后还有一个名为 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
从来没有任何歧义,资源的复数或单数命名是向用户暗示他们可以期待什么(列表或对象)。对 id
没有限制,理论上可以让具有 id new
的用户不与(潜在的未来)子资源名称重叠。
GET /user/{id}/friend
代表什么?我想确保如果您删除 URL 的一部分,仍然会返回资源,继续您的示例,我假设(正确或错误)这将返回用户 {id}
的所有朋友,但这与您使用复数和名词。
/user/{id}/friends
中,它将返回所有朋友。单数版本 /user/{id}/friend
将是一个错误的请求 400,就像 /user
。
使用 Singular 并利用例如“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 只能对单个请求进行单个响应。路径总是指一个单一的东西。
这是“Architectural Styles and the Design of Network-based Software Architectures”的 Roy Fielding 论文,您可能会对此引用感兴趣:
资源是到一组实体的概念映射,而不是在任何特定时间点对应于映射的实体。
作为一种资源,映射到一组实体,对我来说似乎不符合逻辑,使用 /product/
作为访问产品集的资源,而不是 /products/
本身。如果您需要特定产品,则可以访问 /products/1/
。
作为进一步的参考,这个来源有一些关于资源命名约定的文字和例子:
https://restfulapi.net/resource-naming/
至少在一个方面对所有方法使用复数更实用:如果您正在使用 Postman(或类似工具)开发和测试资源 API,则在从 GET 切换到 PUT 到 POST 等时不需要编辑 URI .
我知道大多数人在决定是使用复数还是单数之间犹豫不决。这里没有解决的问题是客户需要知道您使用的是哪一个,而且他们总是可能犯错误。这就是我的建议的来源。
两者怎么样?我的意思是对整个 API 使用单数,然后创建路由以将复数形式的请求转发到单数形式。例如:
GET /resources = GET /resource
GET /resources/1 = GET /resource/1
POST /resources/1 = POST /resource/1
...
你得到图片。没有人是错的,只需最少的努力,客户总能做对。
/resources
并且总是被重定向到 /resource
,那么您做错了。如果其他人使用您的 API,他们可以直接使用正确的 URL 或被重定向(这有效但错误),是您打开了错误的方式。
两种表示方式都很有用。为了方便起见,我使用单数已经有一段时间了,变形可能很困难。我在开发严格单一的 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
创建新资源
对我来说,复数操纵集合,而单数操纵集合中的项目。
集合允许方法 GET / POST / DELETE
项目允许方法 GET / PUT / DELETE
例如
/students 上的 POST 将在学校添加一个新学生。
/students 上的 DELETE 将删除学校中的所有学生。
/student/123 上的 DELETE 将从学校删除学生 123。
这可能感觉不重要,但有些工程师有时会忘记 id。如果路由始终是复数并执行了 DELETE,您可能会不小心擦除数据。而缺少单数上的 id 将返回未找到的 404 路由。
如果 API 应该公开多所学校,为了进一步扩展示例,那么类似
/school/abc/students 上的 DELETE 将删除学校 abc
中的所有学生。
有时选择正确的词本身就是一个挑战,但我喜欢保持收藏的多样性。例如 cart_items
或 cart/items
感觉不错。相反,删除 cart
会删除它自己的购物车对象,而不是购物车中的项目;)。
关于这个问题的很好的讨论点。根据我的经验,命名约定或者不建立本地标准是许多长夜待命、头痛、风险重构、狡猾的部署、代码审查辩论等的根本原因。特别是当它决定事情需要改变是因为一开始没有充分考虑。
一个实际问题跟踪了对此的讨论:
https://github.com/kubernetes/kubernetes/issues/18622
看到这方面的分歧很有趣。
我的两分钱(带有轻微的头痛经验)是,当您考虑用户、帖子、订单、文档等常见实体时,您应该始终将它们视为实际实体,因为这是数据模型的基础。语法和模型实体不应该在这里真正混淆,这会导致其他混淆点。然而,一切都是非黑即白的吗?确实很少如此。上下文真的很重要。
当您希望获取系统中的用户集合时,例如:
获取 /user
->实体用户集合
获取 /user/1
->实体用户资源:1
说我想要一个实体用户集合和说我想要用户集合都是有效的。
获取 /users
->实体用户集合
获取 /users/1
->实体用户资源:1
从这里你说,从用户集合中,给我用户/1
。
但是,如果您分解用户集合是什么... 它是一个实体集合,其中每个实体都是一个 User
实体。
您不会说实体是 Users
,因为单个数据库表通常是 User
的单独记录。但是,我们在这里讨论的是 RESTful 服务而不是数据库 ERM。
但这仅适用于具有明确名词区别的用户,并且易于掌握。但是,当您在一个系统中使用多种相互冲突的方法时,事情会变得非常复杂。
说实话,这两种方法在大多数情况下都是有意义的,除非在少数情况下英语只是意大利面条。它似乎是一种迫使我们做出许多决定的语言!
事情的简单事实是,无论您做出什么决定,都要保持一致和合乎逻辑的意图。
在我看来,在这里混合是一种不好的方法!这悄悄地引入了一些可以完全避免的语义歧义。
看似单一的偏好:
https://www.haproxy.com/blog/using-haproxy-as-an-api-gateway-part-1/
这里有类似的讨论:
这里的首要常数是,根据大公司指南中的细节,它确实似乎确实取决于某种程度的团队/公司文化偏好,两种方式都有许多优点和缺点。谷歌不一定对,只是因为它是谷歌!这适用于任何准则。
避免把你的头埋在沙子里太多,松散地建立你对轶事例子和观点的整个理解系统。
您是否必须为所有事情建立可靠的推理。如果它适合您或您的团队和/我们的客户,并且对新的和经验丰富的开发人员有意义(如果您处于团队环境中),那就太好了。
怎么样:
/resource/
(不是 /resource
)
/resource/
表示它是一个包含名为“resource”的文件夹,它是一个“resouce”文件夹。
而且我认为数据库表的命名约定是相同的,例如,一个名为“用户”的表是一个“用户表”,它包含一个名为“用户”的东西。
我更喜欢同时使用复数 (/resources
) 和单数 (/resource/{id}
),因为我认为它更清楚地区分了处理资源集合和处理单个资源之间的逻辑。
作为一个重要的副作用,它还可以帮助防止有人错误地使用 API。例如,考虑用户错误地尝试通过将 Id 指定为参数来获取资源的情况,如下所示:
GET /resources?Id=123
在这种情况下,如果我们使用复数版本,服务器很可能会忽略 Id 参数并返回所有资源的列表。如果用户不小心,他会认为调用成功并使用列表中的第一个资源。
另一方面,当使用单数形式时:
GET /resource?Id=123
服务器很可能会返回错误,因为没有以正确的方式指定 Id,并且用户将不得不意识到有问题。