英文:
REST API design best practice
问题
这个端点应该放在什么位置以及应该如何设计:
GET /users/1/comments/
或者
GET /comments&userId=1
在 UserController
中我做了以下操作:
@GetMapping("/user/{userId}/comments/")
public ResponseEntity<List<Comment>> getAllComments(@PathVariable Long userId) {
return ResourceUtil.getResponseEntity(Optional.of(userService.getUser(userId).getComments()));
}
或者它应该放在 CommentController
中:
@GetMapping("/comments/")
public ResponseEntity<List<Comment>> getAllComments(@RequestParam Long userId) {
User user = userService.getUser(userId);
List<Comment> comments = commentService.getComments(user);
return ResourceUtil.getResponseEntity(comments);
}
如果我选择遵循这个约定:
GET /users/1/comments/
那么最终我只会得到一个控制器,也就是 UserController
,用于管理所有的资源,因为其他所有资源都可以被视为 /users
的子资源。例如:
GET /users/1/articles/
GET /users/1/jobs/
GET /users/1/applications/
GET /users/1/posts/
等等...
把 /users/
映射到不同的控制器是否是一个好主意呢?这样的情况下:
GET /users/1/articles/
-> ArticlesController
GET /users/1/jobs/
-> JobsController
GET /users/1/applications/
-> ApplicationsController
GET /users/1/posts/
-> PostsController
?
更新
我认为这是不正确的:
GET /users/1/comments/
,因为对于任何给定用户的实体,我们最终都会使用相同的值 /users/1
作为前缀,这是错误的。认证和授权应该是无缝进行的,因此控制器应该能够从请求头部/ JWT 令牌中提取用户信息,并且它们不应该成为 API 的一部分,除非是管理员端点,管理员可以查看任何用户,但这个端点应该是完全独立的。
英文:
Where does this endpoint belong and how should it be:
GET /users/1/comments/
or
GET /comments&userId=1
In UserController
where I do
@GetMapping("/user/{userId}/comments/")
public ResponseEntity<List<Comment>> getAllComments(@PathVariable Long userId) {
return ResourceUtil.getResponseEntity(Optional.of(userService.getUser(userId).getComments()));
}
Or it should be in CommentController
:
@GetMapping("/comments/")
public ResponseEntity<List<Comment>> getAllComments(@RequestParam Long userId) {
User user = userService.getUser(userId);
List<Comment> comments = commentService.getComments(user);
return ResourceUtil.getResponseEntity(comments);
}
?
If I choose to follow this convention:
GET /users/1/comments/
then I end up with only 1 controller - namely UserController
for all resources, as all other resources could be seen as a sub-resource to /users
. For example:
GET /users/1/articles/
GET /users/1/jobs/
GET /users/1/applications/
GET /users/1/posts/
and so on...
Is it a good idea to map /users/
to different controllers so in this case:
GET /users/1/articles/
-> ArticlesController
GET /users/1/jobs/
-> JobsController
GET /users/1/applications/
-> ApplicationsController
GET /users/1/posts/
-> PostsController
?
Update
I think this is not correct:
GET /users/1/comments/
because for any given user's entitties we will end up prefixing them with the same value /users/1
which is wrong. Authentication and Authorization should happen seemlessly, therefore the Controller should be able to extract user information from request headers / JWT token and they should not be part of the API unless it is an admin endpoint and the admin could look at any user, but this endpoint should be completely separate.
答案1
得分: 1
正确的方式是
/users/1/comments/
所有资源都应该由 URL 路径来确定(而不是查询参数)。
只有那些不是资源标识符的数据才应该作为查询参数
/users/1/comments?startsWith=mark&page=3
英文:
Correct way is
/users/1/comments/
All the resources should be identified by URL path (not the query parameters).
Only data that are not resources identifiers should be query parameters
/users/1/comments?startsWith=mark&page=3
答案2
得分: 1
你呈现的每个版本都可以是“正确”的;它们表示了稍微不同的含义。
在第一种情况下,您将comments
视为用户的“子资源”;也就是说,这些评论在其外部没有实际存在意义。如果您要表示用户的身份验证信息或偏好设置等内容,使用这种样式是非常常规的。这种样式相当于使用SQL JOIN,在需要在数据库中使用级联删除时会使用它。
在第二种情况下,您将comments
视为一流的顶级资源,它可以独立存在,仅与用户有关联。查询参数相当于HTTP中的WHERE子句。
是将comments
视为顶级资源还是子资源是您需要做出的设计决策。支持将其作为顶级资源的因素包括始终通过不同的关联进行查询(例如,查询?article=
比查询?user=
更常见!)以及在不参考发布该评论的用户的情况下,是否有意义通过ID引用评论。
英文:
Each of the versions you presented can be "correct"; they mean slightly different things.
In the first case, you are treating comments
as a subresource of the user; that is, the comments have no meaningful existence outside it. It is entirely conventional to use this style if you're representing, say, the user's authentication information or preferences. This style is equivalent to using a SQL JOIN, and you would use it where you might use a cascade delete in your database.
In the second case, you're treating comments
as a first-class top-level resource that stands alone and merely has a relationship to the user. The query parameter is the HTTP equivalent of a WHERE clause.
Whether to treat comments
as a top-level or a subresource is a design decision for you to make. Factors that weigh in favor of a top-level resource include querying by various different relationships all the time (e.g., it's more common to query by ?article=
than ?user=
!) and whether it makes sense to address a comment by ID without reference to the user who posted it.
答案3
得分: 1
REST 不关心您为标识符使用什么拼写,只要它们符合RFC 3986定义的生成规则即可。
GET /users/1/comments/
GET /comments&userId=1
GET /79f541f3-e872-47ba-9ef6-63667e148455
这些都是 fine。
部分要点在于标识符只是标识符;就一般目的组件而言,标识符在语义上是不透明的,因此您可以按需使用它们。
在某些情况下,您可能希望利用相对引用,它允许您操纵标识符的路径段(也称为“分层部分”)以轻松计算其他标识符。
例如,以下这种情况可能很方便
/users/1 == base(/users/1/comments/).resolve(../..)
如果您希望使用RequestParam映射,则需要确保这些键值对出现在URI的查询部分中。换句话说,这两种拼写方式都是可以的(从通用客户端的角度来看)
GET /comments&userId=1
GET /comments?userId=1
但第二种方式在实现路由时会比第一种方式更容易处理。
键/值对的额外优势在于,HTML语义包括表单处理规则,它们知道如何将来自输入控件的数据和元数据转换为应用程序/x-www-form-urlencoded键值对,出现在标识符的查询部分。
请记住,REST API是一个外观;您的实现假装是一个愚蠢的网站,以便您可以利用所有现有的通用组件,这些组件了解如何与愚蠢的网站交互。在网站上,URI只是一个不透明的键,我们用它从存储中获取文档(就像我们可以使用字符串从java.util.Map<String,Value>
中提取值一样)。
英文:
REST doesn't care what spellings you use for your identifiers, so long as they conform to the production rules defined by RFC 3986
GET /users/1/comments/
GET /comments&userId=1
GET /79f541f3-e872-47ba-9ef6-63667e148455
Those are all fine.
Part of the point is that the identifiers are just identifiers; as far as general purpose components are concerned, the identifiers are semantically opaque, so you can do what you want with them.
In some cases, you'll want to take advantage of relative references, which allow you to manipulate the path segments (aka the "hierarchical part") of the identifier to easily compute other identifiers.
For example, it may be convenient that
/users/1 == base(/users/1/comments/).resolve(../..)
If you are expecting to use RequestParam mappings, you need to make sure that the those key value pairs appear in the query part of the URI. In other words, both of these spellings are fine (from the perspective of a general purpose client)
GET /comments&userId=1
GET /comments?userId=1
But the second one is going to be a lot easier to work with than the first when you are implementing your routing.
An extra advantage to key/valye pairs is that HTML semantics include form processing rules that understand how to transform a data and metadata from input controls into application/x-www-form-urlencoded key value pairs in the query part of an identifier.
Remember, a REST API is a facade; your implementation pretends to be a dumb web site so that you can take advantage of any and all of the existing general purpose components that understand how we talk to dumb web sites. On a web site, the URI is just an opaque key that we use to pull a document out of a store (in much the same way that we can use a string to extract a value from a java.util.Map<String,Value>
).
答案4
得分: 1
我对你的问题没有一个确切的答案,因为这实际上取决于你的情况。
考虑一下:一个评论能在没有用户的情况下存在吗?如果评论与某个产品相关,那么评论的所有者是用户还是产品?如果你对此没有答案,你应该问:这个评论更重要的是属于哪个“类别”?如果你失去了一个用户,是否也应该失去这个评论?
在设计 API 时,我总是会问这些问题。希望对你有所帮助。
英文:
I don't have an answer for you, because it really depends on your circumstances.
Consider this: can a comment exist without a user? If the comment is related to a product, who owns the comment, the user or the product? If you don't have an answer to that, you should ask: to which 'class' the comment is more important? If you lose a user, should you lose the comment?
When designing APIs, I always ask these questions. Hope it helps a bit.
答案5
得分: 0
根据惯例,对于每个资源,您需要一个控制器。这是Richardson成熟度模型的第一级。
因此,最佳解决方案是第3种。但我认为userId将是评论表中的外键,因此您可以直接在不从数据库获取用户的情况下使用它。
您可以查看这篇关于Richardson成熟度模型的文章:
Richardson成熟度模型
英文:
By convention, for each resource you need a controller. It is the level 1 of the Richardson Maturity Model.
So the best solution is number 3. But I think userId will be a foreign key in the comment table, so you can use it directly without get the user for the DB.
You can see this article for the Richardson Maturity Model :
Richardson Maturity Model
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论