REST API设计的最佳做法

发布于:2020-12-24 16:05:14

0

65

0

Rest API JavaScript stackoverflow 表达

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响应时,客户端会这样解释,我们应该在发出请求后将响应头中的Content-Type设置为application / json。许多服务器端应用程序框架会自动设置响应头。一些HTTP客户端查看Content-Type响应标头,并根据该格式解析数据。

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

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

让我们看一下一个接受JSON有效负载的API示例。此示例将对Node.js使用Express后端框架。我们可以使用body-parser中间件来解析JSON请求主体,然后可以使用要返回的对象作为JSON响应调用res.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对象。

在对application / json的响应中设置Content-Type标头; 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('服务器已启动'));

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

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


具有多个名词的名称集合

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

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

使用/ articles端点,所有端点都有复数形式,因此我们不必将其更改为复数形式。

嵌套分层对象的资源

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

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

例如,如果我们希望端点获取新闻文章的评论,则应将/ 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('服务器已启动'));

在上面的代码中,我们可以在路径“ / articles /:articleId / comments”上使用GET方法。 我们会获得对由articleId标识的文章的评论,然后在响应中将其返回。 我们在“ / articles /:articleId”路径段之后添加“注释”,以表明它是/ 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');

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('服务器已启动'));

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

然后,如果我们尝试使用用户已经存在的电子邮件值提交有效负载,则会收到带有“用户已存在”消息的400响应状态代码,以告知用户该用户已经存在。 利用这些信息,用户可以通过将电子邮件更改为不存在的邮件来纠正操作。

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

每当我们的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('服务器已启动'));

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

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

/ employees?lastName = Smith&age = 30

我们得到:


[

    {

        "firstName": "John",

        "lastName": "Smith",

        "age": 30

    }

]


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

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

我们还可以在查询字符串中指定要排序的字段。 例如,我们可以从查询字符串中获取参数,其中包含我们要为其排序数据的字段。 然后,我们可以按照这些单独的字段对它们进行排序。例如,我们可能想从URL中提取查询字符串,例如:http://example.com/articles?sort=+author,发布日期,其中+表示上升,而-表示下降。 因此,我们按作者姓名的字母顺序排序,并且发布日期从最近到最近。

保持良好的安全习惯

客户端和服务器之间的大多数通信应该是私有的,因为我们经常发送和接收私有信息。因此,必须使用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('服务器已启动'));

上面的代码仅使用apicache.middleware引用了apicache中间件,然后得到:app.use(缓存('5分钟'))将缓存应用于整个应用。 例如,我们将结果缓存五分钟。 我们可以根据需要进行调整。

版本化我们的API

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

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

通常通过在API路径的开头添加/ v1 /,/ v2 /等来完成版本控制。

例如,我们可以使用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('服务器已启动'));

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

结论

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

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

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