您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

Rest API 最佳设计实践

时间:2022-01-13 09:34:46  来源:  作者:杨同学编程

REST API 是当今可用的最常见的 Web 服务类型之一。它们允许包括浏览器应用程序在内的各种客户端通过 REST API 与服务器通信。因此,正确设计 REST API 非常重要,这样我们以后就不会遇到问题。我们必须考虑 API 使用者的安全性、性能和易用性。

否则,我们会为使用我们 API 的客户制造问题,这不愉快并且会影响人们使用我们的 API。如果我们不遵循普遍接受的约定,那么我们就会混淆 API 的维护者和使用它们的客户端,因为它与每个人的期望不同。

在本文中,我们将研究如何设计 REST API,使其对任何使用它们的人都易于理解、面向未来、安全且快速,因为它们向可能是机密的客户端提供数据。

  • 接受并使用 JSON 响应
  • 在端点路径中使用名词而不是动词
  • 用复数名词命名集合
  • 分层对象的嵌套资源
  • 优雅地处理错误并返回标准错误代码
  • 允许过滤、排序和分页
  • 保持良好的安全实践
  • 缓存数据以提高性能
  • 版本控制我们的 API

什么是 REST API?

REST API 是符合特定架构约束的应用程序编程接口,例如无状态通信和可缓存数据。它不是协议或标准。虽然可以通过多种通信协议访问 REST API,但最常见的是通过 HTTPS 调用它们,因此以下指南适用于将通过 Internet 调用的 REST API 端点。

注意:对于通过 Internet 调用的 REST API,您需要遵循REST API 身份验证的最佳实践。

接受并使用 JSON 响应

REST API 应该接受 JSON 作为请求负载,并发送响应到 JSON。JSON 是传输数据的标准。几乎所有联网技术都可以使用它:JAVAScript 具有通过 Fetch API 或其他 HTTP 客户端对 JSON 进行编码和解码的内置方法。服务器端技术具有无需做太多工作即可解码 JSON 的库。

还有其他方法可以传输数据。如果不将数据自己转换为可以使用的东西(通常是 JSON),框架就不会广泛支持 XML。我们无法在客户端轻松操作这些数据,尤其是在浏览器中。只是为了进行正常的数据传输,最终需要做很多额外的工作。

表单数据有利于发送数据,特别是如果我们要发送文件。但是对于文本和数字,我们不需要表单数据来传输它们,因为对于大多数框架,我们可以通过直接在客户端获取数据来传输 JSON。这是迄今为止最直接的做法。

为确保当我们的 REST API 应用程序使用 JSON 响应时客户端将其解释为这样,我们应该Content-Type在响应标头中设置为Application/json在发出请求之后。许多服务器端应用程序框架会自动设置响应标头。一些 HTTP 客户端查看Content-Type响应标头并根据该格式解析数据。

唯一的例外是如果我们尝试在客户端和服务器之间发送和接收文件。然后我们需要处理文件响应并将表单数据从客户端发送到服务器。但这是另一个话题。

我们还应该确保我们的端点返回 JSON 作为响应。许多服务器端框架将此作为内置功能。

让我们看一个接受 JSON 有效负载的示例 API。此示例将使用Node.js的Express后端框架。我们可以使用body-parser中间件来解析 JSON 请求体,然后我们可以res.json使用我们想要作为 JSON 响应返回的对象调用该方法,如下所示:

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.post('/', (req, res) => {
  res.json(req.body);
});

app.listen(3000, () => console.log('server started'));

bodyParser.json()将 JSON 请求正文字符串解析为 JavaScript 对象,然后将其分配给该req.body对象。

将Content-Type响应中的标头设置为application/json; charset=utf-8不做任何更改。上述方法适用于大多数其他后端框架。

在端点路径中使用名词而不是动词

我们不应该在端点路径中使用动词。相反,我们应该使用代表我们正在检索或操作的端点的实体的名词作为路径名。

这是因为我们的 HTTP 请求方法已经有了动词。在我们的 API 端点路径中包含动词没有用处,而且由于它没有传达任何新信息,它会导致不必要的冗长。选择的动词可能会因开发人员的突发奇想而有所不同。例如,有些喜欢“get”,有些喜欢“retrieve”,所以最好让 HTTP GET 动词告诉我们端点做什么。

该操作应由我们正在制作的 HTTP 请求方法指示。最常见的方法包括 GET、POST、PUT 和 DELETE。

  • GET 检索资源。
  • POST 向服务器提交新数据。
  • PUT 更新现有数据。
  • DELETE 删除数据。

动词映射到CRUD操作。

考虑到我们上面讨论的两个原则,我们应该创建像 GET 这样的路由/articles/来获取新闻文章。同样,POST/articles/用于添加新文章,PUT/articles/:id用于使用给定的更新文章id。DELETE/articles/:id用于删除具有给定 ID 的现有文章。

/articles表示 REST API 资源。例如,我们可以使用 Express 添加以下端点来操作文章,如下所示:

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.get('/articles', (req, res) => {
  const articles = [];
  // code to retrieve an article...
  res.json(articles);
});

app.post('/articles', (req, res) => {
  // code to add a new article...
  res.json(req.body);
});

app.put('/articles/:id', (req, res) => {
  const { id } = req.params;
  // code to update an article...
  res.json(req.body);
});

app.delete('/articles/:id', (req, res) => {
  const { id } = req.params;
  // code to delete an article...
  res.json({ deleted: id });
});

app.listen(3000, () => console.log('server started'));

在上面的代码中,我们定义了操作文章的端点。正如我们所见,路径名中没有任何动词。我们只有名词。动词在 HTTP 动词中。

POST、PUT 和 DELETE 端点都将 JSON 作为请求体,它们都返回 JSON 作为响应,包括 GET 端点。

在端点上使用逻辑嵌套

在设计端点时,将包含相关信息的端点分组是有意义的。也就是说,如果一个对象可以包含另一个对象,您应该设计端点来反映这一点。无论您的数据在数据库中的结构是否如此,这都是一种很好的做法。事实上,避免在端点中镜像数据库结构以避免给攻击者提供不必要的信息可能是明智的。

例如,如果我们希望端点获取新闻文章的评论,我们应该将/comments路径附加到路径的末尾/articles。我们可以在 Express 中使用以下代码来做到这一点:

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.get('/articles/:articleId/comments', (req, res) => {
  const { articleId } = req.params;
  const comments = [];
  // code to get comments by articleId
  res.json(comments);
});


app.listen(3000, () => console.log('server started'));

在上面的代码中,我们可以在 path 上使用 GET 方法'
/articles/:articleId/comments'。我们获取comments由标识的文章articleId,然后在响应中返回它。我们'comments'在'/articles/:articleId'路径段之后添加以表明它是 的子资源/articles。

这是有道理的,因为comments是 的子对象articles,假设每篇文章都有自己的评论。否则,用户会感到困惑,因为这种结构通常被认为是用于访问子对象。同样的原则也适用于 POST、PUT 和 DELETE 端点。它们都可以对路径名使用相同类型的嵌套结构。

但是,嵌套可能会走得太远。在大约第二或第三级之后,嵌套端点可能会变得笨拙。相反,请考虑将 URL 返回到这些资源,特别是如果该数据不一定包含在顶级对象中。

例如,假设您想返回特定评论的作者。你可以使用
/articles/:articleId/comments/:commentId/author. 但这已经失控了。而是在 JSON 响应中返回该特定用户的 URI:

"author": "/users/:userId"

优雅地处理错误并返回标准错误代码

为了在错误发生时消除 API 用户的困惑,我们应该优雅地处理错误并返回指示发生了哪种错误的 HTTP 响应代码。这为 API 的维护者提供了足够的信息来了解发生的问题。我们不希望错误导致我们的系统崩溃,所以我们可以不处理它们,这意味着 API 使用者必须处理它们。

常见的错误 HTTP 状态代码包括:

  • 400 Bad Request – 这意味着客户端输入验证失败。
  • 401 Unauthorized – 这意味着用户无权访问资源。它通常在用户未通过身份验证时返回。
  • 403 Forbidden - 这意味着用户已通过身份验证,但不允许访问资源。
  • 404 Not Found – 这表示未找到资源。
  • 500 内部服务器错误 – 这是一般的服务器错误。它可能不应该明确抛出。
  • 502 Bad Gateway - 这表示来自上游服务器的无效响应。
  • 503 Service Unavailable - 这表明服务器端发生了意外(可能是服务器过载,系统某些部分发生故障等)。

我们应该抛出与我们的应用程序遇到的问题相对应的错误。例如,如果我们想拒绝来自请求负载的数据,那么我们应该在 Express API 中返回一个 400 响应,如下所示:

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

// existing users
const users = [
  { email: 'abc@foo.com' }
]

app.use(bodyParser.json());

app.post('/users', (req, res) => {
  const { email } = req.body;
  const userExists = users.find(u => u.email === email);
  if (userExists) {
    return res.status(400).json({ error: 'User already exists' })
  }
  res.json(req.body);
});


app.listen(3000, () => console.log('server started'));

在上面的代码中,我们users在给定电子邮件的数组中有一个现有用户的列表。

然后,如果我们尝试使用email中已存在的值提交有效负载users,我们将获得一个 400 响应状态码和一条'User already exists'消息,让用户知道该用户已经存在。使用该信息,用户可以通过将电子邮件更改为不存在的内容来更正操作。

错误代码需要带有消息,以便维护者有足够的信息来解决问题,但攻击者不能使用错误内容来进行我们的攻击,如窃取信息或关闭系统。

每当我们的 API 没有成功完成时,我们应该通过发送错误信息来帮助用户做出纠正措施,从而优雅地失败。

允许过滤、排序和分页

REST API 背后的数据库可能会变得非常大。有时,有太多的数据不应该一次全部返回,因为它太慢或者会导致我们的系统崩溃。因此,我们需要过滤项目的方法。

我们还需要对数据进行分页的方法,以便一次只返回几个结果。我们不想通过尝试一次获取所有请求的数据来占用资源太久。

过滤和分页都通过减少服务器资源的使用来提高性能。随着数据库中积累的数据越多,这些特征就越重要。

这是一个小示例,其中 API 可以接受具有各种查询参数的查询字符串,以便我们按字段过滤项目:

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

// employees data in a database
const employees = [
  { firstName: 'Jane', lastName: 'Smith', age: 20 },
  //...
  { firstName: 'John', lastName: 'Smith', age: 30 },
  { firstName: 'Mary', lastName: 'Green', age: 50 },
]

app.use(bodyParser.json());

app.get('/employees', (req, res) => {
  const { firstName, lastName, age } = req.query;
  let results = [...employees];
  if (firstName) {
    results = results.filter(r => r.firstName === firstName);
  }

  if (lastName) {
    results = results.filter(r => r.lastName === lastName);
  }

  if (age) {
    results = results.filter(r => +r.age === +age);
  }
  res.json(results);
});

app.listen(3000, () => console.log('server started'));

在上面的代码中,我们有req.query获取查询参数的变量。然后,我们通过使用 JavaScript 解构语法将各个查询参数解构为变量来提取属性值。最后,我们filter继续使用每个查询参数值来定位我们想要返回的项目。

完成此操作后,我们将返回results作为响应。因此,当我们使用查询字符串向以下路径发出 GET 请求时:

/employees?lastName=Smith&age=30

我们得到:

[
    {
        "firstName": "John",
        "lastName": "Smith",
        "age": 30
    }
]

作为我们过滤后的返回响应lastName和age。

同样,我们可以接受page查询参数并返回位置 from (page - 1) * 20to中的一组条目page * 20。

我们还可以在查询字符串中指定要排序的字段。例如,我们可以从带有我们想要对其数据进行排序的字段的查询字符串中获取参数。然后我们可以按这些单独的字段对它们进行排序。

例如,我们可能想从如下 URL 中提取查询字符串:

http://example.com/articles?sort=+author,-datepublished

where+表示上升,-表示下降。所以我们按照作者姓名的字母顺序,datepublished从最近到最近的排序。

保持良好的安全实践

客户端和服务器之间的大多数通信应该是私密的,因为我们经常发送和接收私人信息。因此,必须使用 SSL/TLS 来确保安全。

将 SSL 证书加载到服务器上并不难,而且成本免费或非常低。没有理由不让我们的 REST API 通过安全通道而不是公开方式进行通信。

人们不应该能够访问他们请求的更多信息。例如,普通用户不应该能够访问其他用户的信息。他们也不应该能够访问管理员的数据。

为了执行最小权限原则,我们需要为单个角色添加角色检查,或者为每个用户添加更细化的角色。

如果我们选择将用户分组为几个角色,那么这些角色应该具有涵盖他们需要的所有权限,仅此而已。如果我们对用户可以访问的每个功能都拥有更精细的权限,那么我们必须确保管理员可以相应地从每个用户添加和删除这些功能。此外,我们需要添加一些可以应用于组用户的预设角色,这样我们就不必手动为每个用户执行此操作。

缓存数据以提高性能

我们可以添加缓存以从本地内存缓存返回数据,而不是每次想要检索用户请求的一些数据时都查询数据库来获取数据。缓存的好处是用户可以更快地获取数据。但是,用户获得的数据可能已经过时。当我们不断看到旧数据时出现问题时,这也可能导致在生产环境中调试时出现问题。

缓存解决方案有很多种,比如redis、内存缓存等等。我们可以随着需求的变化而改变缓存数据的方式。

例如,Express 具有apicache无需太多配置即可将缓存添加到我们的应用程序的中间件。我们可以像这样在我们的服务器中添加一个简单的内存缓存:

const express = require('express');
const bodyParser = require('body-parser');
const apicache = require('apicache');
const app = express();
let cache = apicache.middleware;
app.use(cache('5 minutes'));

// employees data in a database
const employees = [
  { firstName: 'Jane', lastName: 'Smith', age: 20 },
  //...
  { firstName: 'John', lastName: 'Smith', age: 30 },
  { firstName: 'Mary', lastName: 'Green', age: 50 },
]

app.use(bodyParser.json());

app.get('/employees', (req, res) => {
  res.json(employees);
});

app.listen(3000, () => console.log('server started'));

上面的代码只是引用了apicache中间件,apicache.middleware然后我们有:

app.use(cache('5 minutes'))

将缓存应用于整个应用程序。例如,我们将结果缓存五分钟。我们可以根据需要进行调整。

如果您使用缓存,您还应该Cache-Control在标题中包含信息。这将帮助用户有效地使用您的缓存系统。

版本控制我们的 API

如果我们对它们进行任何可能破坏客户端的更改,我们应该有不同版本的 API。可以像现在大多数应用程序一样,根据语义版本(例如,2.0.6 表示主要版本 2 和第六个补丁)进行版本控制。

这样,我们可以逐步淘汰旧的端点,而不是强迫每个人同时迁移到新的 API。v1 端点可以为不想改变的人保持活跃,而 v2 具有闪亮的新功能,可以为那些准备升级的人提供服务。如果我们的 API 是公开的,这一点尤其重要。我们应该对它们进行版本控制,这样我们就不会破坏使用我们 API 的第三方应用程序。

版本通常有做/v1/,/v2/等在API路径的开始增加。

例如,我们可以使用 Express 执行此操作,如下所示:

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());

app.get('/v1/employees', (req, res) => {
  const employees = [];
  // code to get employees
  res.json(employees);
});

app.get('/v2/employees', (req, res) => {
  const employees = [];
  // different code to get employees
  res.json(employees);
});

app.listen(3000, () => console.log('server started'));

我们只需将版本号添加到端点 URL 路径的开头即可对其进行版本控制。

结论

设计高质量 REST API 最重要的一点是通过遵循 Web 标准和约定来保持一致性。JSON、SSL/TLS 和 HTTP 状态代码都是现代 Web 的标准构建块。

性能也是一个重要的考虑因素。我们可以通过不一次返回太多数据来增加它。此外,我们可以使用缓存,这样我们就不必一直查询数据。

端点的路径应该是一致的,我们只使用名词,因为 HTTP 方法表明我们想要采取的行动。嵌套资源的路径应该在父资源的路径之后。他们应该告诉我们我们正在获取或操作什么,而无需阅读额外的文档来了解它在做什么。



Tags:API   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
REST API 是当今可用的最常见的 Web 服务类型之一。它们允许包括浏览器应用程序在内的各种客户端通过 REST API 与服务器通信。因此,正确设计 REST API 非常重要,这样我们以后...【详细内容】
2022-01-13  Tags: API  点击:(0)  评论:(0)  加入收藏
最近需要做一个打印的功能,于是在网上找到了这么一个方法。   [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public class DOCINFOA { [M...【详细内容】
2022-01-04  Tags: API  点击:(18)  评论:(0)  加入收藏
近日只是为了想尽办法为 Flask 实现 Swagger UI 文档功能,基本上要让 Flask 配合 Flasgger, 所以写了篇 Flask 应用集成 Swagger UI 。然而不断的 Google 过程中偶然间发现了...【详细内容】
2021-12-23  Tags: API  点击:(26)  评论:(0)  加入收藏
最近一连串的 API 安全事件(Peloton、Experian、Clubhouse 等)无疑迫使许多安全和开发团队仔细检查他们的 API 安全状况,以确保它们不会成为下一个被攻击对象。创建面向外部受...【详细内容】
2021-09-01  Tags: API  点击:(62)  评论:(0)  加入收藏
一直在写东西,有时候会遇到这种需求,就是把图片上的文字拷贝到自己的文章中,所以写了这个小工具。 配合Snipaste使用天衣无缝,所有的东西都在剪切板里交换,即Snipaste截取的图片...【详细内容】
2021-08-30  Tags: API  点击:(72)  评论:(0)  加入收藏
过去几个月里,我一直在对付一个流行健身品牌的 API,最后发现自己陷入了一种卡夫卡式的噩梦。程序员都喜欢挑战,优秀的程序员一定要征服种种挑战。我一直觉得自己是一个非常优秀...【详细内容】
2021-07-22  Tags: API  点击:(102)  评论:(0)  加入收藏
作者 | edmz译者 | 王强策划 | 万佳多年来,我已经为很多 API 实现了客户端。为此,我整理了一份清单,列出了一些可以改善开发体验的小技巧。这些想法大都与 API 设计或架构无关...【详细内容】
2021-06-29  Tags: API  点击:(140)  评论:(0)  加入收藏
创建一个 API(应用程序接口),我们所要做的远远不止是让它能“正常工作”。如果你正在构建基于 C/S 模型的应用程序,那么你需要一个应用程序接口(API)。API 就是一种非常清晰而又明...【详细内容】
2021-06-16  Tags: API  点击:(152)  评论:(0)  加入收藏
FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 3.6+ 并基于标准的 Python 类型提示。关键特性: 快速:可与 NodeJS 和 Go 比肩的极高性能(归功于 Star...【详细内容】
2021-05-10  Tags: API  点击:(390)  评论:(0)  加入收藏
今天又要给大家介绍一个 Spring Boot 中的组件 --HandlerMethodReturnValueHandler。在前面的文章中(如何优雅的实现 Spring Boot 接口参数加密解密?),松哥已经和大家介绍过如何...【详细内容】
2021-03-24  Tags: API  点击:(300)  评论:(0)  加入收藏
▌简易百科推荐
REST API 是当今可用的最常见的 Web 服务类型之一。它们允许包括浏览器应用程序在内的各种客户端通过 REST API 与服务器通信。因此,正确设计 REST API 非常重要,这样我们以后...【详细内容】
2022-01-13  杨同学编程    Tags:API   点击:(0)  评论:(0)  加入收藏
CPU对我们来说既熟悉又陌生,熟悉的是我们知道代码是被CPU执行的,当我们的线上服务出现问题时可能首先会查看CPU负载情况。陌生的是我们并不知道CPU是如何执行代码的,它对我们的...【详细内容】
2022-01-06  小心程序猿QAQ    Tags:代码   点击:(8)  评论:(0)  加入收藏
前言小黑在开发中遇到个问题,我负责的模块需要调用某个三方服务接口查询信息,查询结果直接影响后续业务逻辑的处理;这个接口偶尔会因网络问题出现超时,导致我的业务逻辑无法继续...【详细内容】
2022-01-04  小黑说Java    Tags:接口重试   点击:(12)  评论:(0)  加入收藏
我在会议演讲后经常收到的一个问题很奇怪,不是关于我演讲的内容,而是关于我的Linux桌面环境。人们对这个漂亮的发行版更感兴趣,而不是我刚才做的精彩演示我不是在抱怨,我喜欢我...【详细内容】
2022-01-04  木偶跳舞    Tags:Linux 开发   点击:(12)  评论:(0)  加入收藏
写在前面有时候可能做项目组长,负责一个项目开发,但是工作是内网,也没有公司的版本库权限,那这个时候,我们怎么用处理版本控制,可以用集中式的版本库工具SVN,或者分布式的Git,这里和...【详细内容】
2021-12-31  山河已无恙被注册了    Tags:Git版本库   点击:(26)  评论:(0)  加入收藏
1、什么是YAMLYAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写。YAML的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。主要强度...【详细内容】
2021-12-29  编程菌zfn    Tags:YAML   点击:(14)  评论:(0)  加入收藏
本文分为三个等级自顶向下地分析了glibc中内存分配与回收的过程。本文不过度关注细节,因此只是分别从arena层次、bin层次、chunk层次进行图解,而不涉及有关指针的具体操作。前...【详细内容】
2021-12-28  linux技术栈    Tags:glibc   点击:(12)  评论:(0)  加入收藏
摘 要 (OF作品展示)OF之前介绍了用python实现数据可视化、数据分析及一些小项目,但基本都是后端的知识。想要做一个好看的可视化大屏,我们还要学一些前端的知识(vue),网上有很多比...【详细内容】
2021-12-27  项目与数据管理    Tags:Vue   点击:(26)  评论:(0)  加入收藏
程序是如何被执行的  程序是如何被执行的?许多开发者可能也没法回答这个问题,大多数人更注重的是如何编写程序,却不会太注意编写好的程序是如何被运行,这并不是一个好...【详细内容】
2021-12-23  IT学习日记    Tags:程序   点击:(29)  评论:(0)  加入收藏
阅读收获✔️1. 了解单点登录实现原理✔️2. 掌握快速使用xxl-sso接入单点登录功能一、早期的多系统登录解决方案 单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器...【详细内容】
2021-12-23  程序yuan    Tags:单点登录(   点击:(19)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条