设计一个易于使用且灵活的 REST API
如果您已经构建了一个使用 REST API 的应用程序,并且负责后端接口,那么您可能已经想知道如何设计您的 URL、采用什么约定以及如何保持这个 API 简单易用而不会使其越来越难以维护。
我为那些正在寻找让 REST API 变得有趣的技巧的人们创建了这篇文章。
今天的菜单上有:
从良好的数据库设计开始
如果您的客户端应用程序难以轻松获取所需的数据,因为它需要调用几个相互依赖的路由,或者这些调用没有多大意义,或者这些调用彼此冗余,那么这可能是一个机会来考虑另一种设计数据库的方式。
您可以遵循以下建议来帮助您维护良好设计的数据库:
- 确保实体之间没有重复连接,也没有重复循环连接
- 您的实体仅包含与其相关的数据,而不包含与其他依赖实体相关的数据
- 安全地添加新的依赖项,而无需对现有表进行大量返工
为了帮助您找到更好的数据库模式,数据库规范化是一个可以帮助您确定数据库模式是否一致的工具。
@lorrli274出色地解释了数据库范式原则的最初几个层次。显然,我已经把它列入了我的阅读清单,你也应该看看😉
让我们看一个应用这些原则的例子。
示例:Todo 应用
我发现这个待办事项应用的例子足够灵活,可以用来探索我们之前提到的那些原则。让我们深入研究一下。
我们会看到:
1. 数据库模式
首先,让我们总结一下业务需求。
- 用户
- 用户可以创建用户
- 用户可以查看用户的详细信息,包括其分配的任务
- 用户可以查看用户列表
- 用户可以编辑用户
- 用户可以删除用户
- 任务
- 用户可以创建任务
- 任务可以将用户分配给任务
- 用户可以查看任务的详细信息,包括分配的用户
- 用户可以查看所有任务的列表
- 用户可以编辑任务
- 用户可以删除任务
- 用户可以取消分配给用户的任务
我将CRUD概念映射到我们的待办事项应用程序。
通过这些语句,我们可以按照以下方式创建数据库。
为了帮助您创建可维护的数据库模式,您可以将表列想象为限定它们所代表的实体的一种方式。
如果您发现自己添加的列不代表您的实体,则意味着这里不是添加这些列的正确位置。
2. REST 端点
REST 协议将帮助我们创建能够准确表示实体的 URL。以下是我们数据库中的一个实现示例。
- 用户
- GET
/api/user
获取所有用户列表 - GET
/api/user/{id}
获取用户的详细信息 - GET
/api/user/{id}/task
获取分配给该用户的所有任务的列表 - POST
/api/user
创建新用户 - PUT
/api/user/{id}
更新用户 - 删除
/api/user/{id}
删除用户
- GET
- 任务
- GET
/api/task
获取所有任务列表 - GET
/api/task/{id}
获取任务的详细信息 - 获取
/api/task/{id}/user
分配到此任务的所有用户的列表 - POST
/api/task
创建新任务 - POST
/api/task/{id}/user/{id}
将现有用户附加到任务 - PUT
/api/task/{id}
更新任务 - 删除
/api/task/{id}
删除任务 - 删除
/api/task/{id}/user/{id}
从任务中分离现有用户
- GET
因为我们的模式是“原子的”(表的列是相关的),所以您可以看到我们的 REST 端点是有意义的。
从这些 URL 中,您可以想象您的 UI,例如能够向用户呈现用户列表(GET /api/user
),以便他/她可以选择将此任务分配给哪个用户(POST /api/task/{id}/user/{id}
)。
需要注意的是,有些人会试图这样设计 API,让我们能够通过/api/task/{id}
. ...
- 一旦您想要过滤服务器返回的数据,您将被迫使用一种语法来区分实体的字段(例如,您只想检索任务和用户的 ID,您将编写类似于的内容
/api/task/{id}?select=task.id,task.user.id
),这将使解析服务器端查询字符串的任务更加复杂 - 作为一般规则,您不应该在返回实体详细信息的路由中检索 1-N 关系(
/api/user/{id}
,/api/task/{id}
),因为您的 Web 应用程序的用户可能不需要此信息,所以不要浪费带宽和 CPU 时间,最好的办法是在您的用户界面中提出一个按钮来访问此信息
3. 设计非 CRUD 命令
据我所知,CRUD 概念中最常见的模式是删除/恢复用户。这并非删除操作,因为数据仍然存在于数据库中,因此如果通过 DELETE 协议处理这些操作,则操作将不正确。然而,这可以被视为一种抑制操作,因为它阻止查看已删除的实体,并且需要通过特殊路径 ( /api/user?filter=active eq false
) 获取它们。
为了避免这个问题,让我们更新数据库模式。
从现在开始,当您想将用户放入垃圾箱时,您可以使用 PUT 方法并将 active 列传递给 false。
PUT /api/user/{id} HTTP 2.0
Host: example.com
Content-Type: application/json
Content-Length: 21
{
"active": false
}
另外,不要让用户从编辑表单中删除实体。此操作比简单的修改更重要,请将其放在单独且专用的位置,例如单击按钮时。
4. 是否应在表中存储时间戳?
许多框架提供了创建字段的快捷方式,让您知道实体何时被创建和修改。
例如,在 Laravel 中,您可能在迁移中使用过它。
Schema::create("task", function(Blueprint $table) {
$table->increments("id");
$table->string("title");
$table->string("description");
$table->timestamps(); // <---
});
这将创建表及其列,并添加另外 2 个列:created_at
和updated_at
。
这里我们有两个问题:
- 如果我们想知道谁创建了这个任务,我们需要添加一个垃圾“created_by”列,这会破坏表的原子性
- 如果我编辑这个任务 4 次,谁会知道编辑历史(除了你的猫)?
出于所有这些原因,我喜欢以原子的方式思考。如果您需要追踪谁创建、修改和删除了您的实体,这意味着您需要将此需求建模为单独的表。
通过这种方式对历史记录进行建模,您将允许用户按需浏览所选实体的更改列表。关联的 REST 端点将是 GET /api/task/{id}/history
,这很合理。
有些人可能会争辩说添加history_type
表是没有用的,因为我们知道我们正在处理一个有限的列表:“创建”,“版本”,“删除”。
不幸的是,如果您决定在表中保留可枚举类型,则还必须将这些值复制到客户端应用程序中,因为您无法从数据库中检索它们。
例如,如果您需要将它们显示给用户,以便他只能看到“删除”类型的更改,则代码重复会使您的应用程序更难维护(如果将“删除”更改为“删除”,您会乐意在两个不同的地方进行更改吗?)。
之前/api/task/{id}/history?filter=historyType eq delete
之后/api/task/{id}/history?filter=historyTypeId eq 3
得到/api/history-type
[
{"id": 1, "name": "creation"},
{"id": 2, "name": "edition"},
{"id": 3, "name": "deletion"}
]
<option id="1">creation</option>
<option id="2">edition</option>
<option id="3">deletion</option>
如您所见,我介绍了一种奇怪的方法来过滤来自我们之前见过的 GET 端点的数据,使用了这个?filter=...
语法。我的灵感来自于OData v4 - URL 约定,这将是本文最后一部分的完美过渡。
如何自定义服务器响应?
我认为 GraphQL 最大的附加值是能够以灵活的方式请求服务器,以便您可以精确地定位要检索的列和关系。
如果有机会,就看看这个精彩的概念,它是值得的:GraphQL 简介。
同时,回到 REST 世界,GraphQL 优雅地解决了这个问题:能够自定义服务器响应。
想象一下,你正在开发 Web 应用中的任务列表视图。你想添加一个鼠标悬停效果,让用户可以预览任务描述的前 50 个字符。太棒了!
另一方面,Android 团队正在构建相同的应用程序,但由于该应用程序针对的是移动用户,他们选择仅显示任务名称,而不显示任何点击工具提示效果。
两个团队都需要向服务器查询/api/task
端点响应(通过 GET 方式)。只有 Android 团队会遇到性能问题,因为他们会毫无意义地获取每个任务的描述。
OData v4 协议
在办公室,我使用Microsoft Graph API将我们的用户连接到他们的 Outlook 帐户,以便他们无需离开我们的 Web 应用程序即可查看他们的电子邮件。
我喜欢这个 API,因为它提供了一种使用 OData 协议灵活检索电子邮件及其相关数据的方法。例如,你可以通过 ID 获取特定的电子邮件:
GET https://graph.microsoft.com/v1.0/me/messages/AAMkADhMGAAA=
您还可以自定义检索的字段:
GET https://graph.microsoft.com/v1.0/me/messages/AAMkADhAAAW-VPeAAA=/?$select=internetMessageHeaders
OData 协议添加了有用的帮助程序(参见文档)来操作 REST 端点处理的数据。
为了能够响应需要使用此协议自定义服务器响应的客户端请求,您应该在返回结果之前添加一个逻辑层。幸运的是,一些优秀的开源工具可以帮助我们快速上手,例如NodeJS 的odata-parser 。
在我看来,这个协议本身有点难以理解。本文的这一部分欢迎大家提出任何关于如何将该协议顺利集成到现有框架的建议。
结论
我认为构建 REST API 非常令人兴奋。我非常欣赏任何设计良好的 API,因为当我尝试将它添加到我的任何 Web 应用中时,它很快就会成为真正的宝贵资源。
当您处理复杂的表格时,过滤和自定义服务器响应的机会可能会改变游戏规则,因为您可以节省一些宝贵的字节和客户端解析时间。
如果您能驾驭这项技术,OData 提供了一种解决这个问题的绝佳方法。请告诉我您对这个协议的看法,并告诉我您是否使用它或类似的模式来处理服务器端响应管理。
就这些了,我希望你们能学到一些东西,我很高兴像往常一样在 Dev.to 上写作,所以请继续关注未来的文章,同时,照顾好自己!
优化愉快!
您所看到的所有图表均由免费的Draw.io网络应用程序制作。
封面照片由Pixabay的Lorenzo Cafaro拍摄。
文章来源:https://dev.to/anwar_nairi/design-an-easy-to-use-and-flexible-rest-endpoints-3fia