REST API设计的最佳做法

REST API是当今最常见的Web服务之一。它们允许包括浏览器应用程序在内的各种客户端通过REST API与服务器进行通信。

因此,正确设计REST API非常重要,这样我们就不会遇到麻烦。我们必须考虑API使用者的安全性,性能和易用性。

否则,我们会为使用我们的API的客户带来问题,这令人不愉快,并且会妨碍人们使用我们的API。如果我们不遵循公认的约定,那么我们会混淆API的维护者和使用它们的客户端,因为它与每个人的期望都不同。

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

由于联网应用程序有多种破坏方法,因此我们应确保使用标准的HTTP代码,任何REST API都会优雅地处理错误,以帮助消费者解决问题。

在这里插入图片描述

接受并使用JSON进行响应

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

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

表单数据非常适合发送数据,尤其是当我们要发送文件时。但是对于文本和数字,我们不需要表单数据来传输它们,因为在大多数框架中,我们可以通过直接从客户端获取JSON来传输JSON。到目前为止,这是最直接的方法。

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

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

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

让我们看一个接受JSON负载的示例API。本示例将对Node.js 使用Express后端框架。我们可以使用中间件来解析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'));

在端点路径中使用名词代替动词

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

这是因为我们的HTTP请求方法已经具有动词。在我们的API端点路径中使用动词是没有用的,并且由于它不会传达任何新信息,因此使它变得不必要地冗长。所选动词可能会因开发人员的想法而异。例如,有些像“ get”,有些像“ retrieve”,所以最好让HTTP GET动词告诉我们做什么和端点做什么。

该动作应由我们正在执行的HTTP请求方法指示。最常见的方法包括GET,POST,PUT和DELETE。

GET检索资源。POST将新数据提交到服务器。PUT更新现有数据。DELETE删除数据。这些动词映射到CRUD操作。

牢记上面讨论的两个原则,我们应该创建诸如GET之类的路线/articles/来获取新闻文章。同样,POST /articles/用于添加新文章,PUT 用于使用给定的文章更新文章。DELETE 用于删除具有给定ID的现有文章。

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

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

const app = express();const app = express();

app.use(bodyParser.json());.use(bodyParser.json());

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

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

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

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

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

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

POST,PUT和DELETE端点都将JSON作为请求正文,并且都返回JSON作为响应,包括GET端点。

具有多个名词的名称集合

我们应该用复数名词来命名集合。我们不经常只想得到一个项目,因此我们应该与命名保持一致,应该使用复数名词。

我们使用复数来与数据库中的内容保持一致。表通常具有多个条目,并对其进行命名以反映这一点,因此,为了与表保持一致,我们应该使用与API访问的表相同的语言。

对于端点,我们对所有端点都有复数形式,因此我们不必将其更改为复数。

嵌套分层对象的资源

处理嵌套资源的端点的路径应通过将嵌套资源附加为父资源后面的路径的名称来完成。

我们必须确保它确保我们认为嵌套资源与数据库表中的资源匹配。否则,将造成混乱。

例如,如果我们希望端点获取新闻文章的评论,则应将路径附加到路径的末尾。假设我们在数据库中拥有的子项。/comments/articlescommentsarticle

例如,我们可以使用Express中的以下代码来做到这一点:

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

const app = express();const app = express();

app.use(bodyParser.json());.use(bodyParser.json());

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


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

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

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

妥善处理错误并返回标准错误代码

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

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

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

我们应该抛出与我们的应用程序遇到的问题相对应的错误。例如,如果我们要拒绝请求有效载荷中的数据,则应在Express API中返回如下所示的400响应:

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

const app = express();const app = express();

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

app.use(bodyParser.json());.use(bodyParser.json());

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


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

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

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

错误代码需要附带消息,以便维护人员拥有足够的信息来解决问题,但是攻击者无法使用错误内容来进行攻击,例如窃取信息或关闭系统。

每当我们的API未成功完成时,我们都应该通过发送错误信息并帮助用户采取纠正措施来正常地失败。

允许过滤,排序和分页

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

我们还需要分页数据的方式,以便一次只返回一些结果。我们不想通过尝试一次获取所有请求的数据来占用资源太长时间。

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

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

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

const app = express();const app = express();

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

app.use(bodyParser.json());.use(bodyParser.json());

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

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

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

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

在上面的代码中,我们有一个变量来获取查询参数。然后,我们通过使用JavaScript解构语法将各个查询参数解构为变量来提取属性值。最后,我们对每个查询参数值进行操作,以找到要返回的项目。req.queryfilter

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

/employees?lastName=Smith&age=30

我们得到:

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

作为返回的响应,因为我们通过lastName和进行了过滤age。

同样,我们可以接受pagequery参数,并从到返回位置的一组条目。 (page - 1) * 20page * 20

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

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

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

其中+表示上升和-表示下降。因此,我们按作者姓名的字母顺序(datepublished从最近到最近)进行排序。

保持良好的安全习惯

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

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

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

为了实施最小特权原则,我们需要为单个角色添加角色检查,或者为每个用户添加更精细的角色。

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

缓存数据以提高性能

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

有许多种缓存解决方案,例如Redis,内存中缓存等等。随着需求的变化,我们可以更改数据缓存的方式。

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

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

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

app.use(bodyParser.json());.use(bodyParser.json());

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

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

上面的代码仅引用了apicache中间件,然后得到:apicache.middleware

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

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

版本化我们的API

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

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

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

例如,我们可以使用Express进行如下操作:

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

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

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

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

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

结论

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

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

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

关注公众号,可获取免费编程资源
在这里插入图片描述

每天进步一点点,开心也多一点点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

white_poland

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值