REST API是当今最常见的Web服务之一。它们允许包括浏览器应用程序在内的各种客户端通过REST API与服务器进行通信。
因此,正确设计REST API非常重要,这样我们就不会遇到麻烦。我们必须考虑API使用者的安全性,性能和易用性。
否则,我们会为使用我们的API的客户带来问题,这令人不愉快,并且会妨碍人们使用我们的API。如果我们不遵循公认的约定,那么我们将混淆API的维护者和使用它们的客户端,因为它与每个人的期望都不同。
在本文中,我们将研究如何设计REST API,以使使用它们的任何人都易于理解,面向未来,安全且快速,因为它们将数据提供给可能是机密的客户端。
因为联网应用程序有多种破坏方法,所以我们应确保使用标准的HTTP代码,任何REST API都会优雅地处理错误,以帮助消费者解决问题。
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请求主体,然后可以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(需求主体);});app.listen(3000,()=> console.log('服务器已启动'));
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 article = []; //检索文章的代码... res.json(文章);});app.post('/ articles',(req,res)=> { //添加新文章的代码... res.json(需求主体);});app.put('/ articles /:id',(req,res)=> { const {id} = req.params; //更新文章的代码... res.json(需求主体);});app.delete('/ articles /:id',(req,res)=> { const {id} = req.params; //删除文章的代码... res.json({已删除:id});});app.listen(3000,()=> console.log('服务器已启动'));
在上面的代码中,我们定义了端点来操纵文章。如我们所见,路径名中没有任何动词。我们只有名词。这些动词在HTTP动词中。
POST,PUT和DELETE端点都将JSON作为请求正文,并且都返回JSON作为响应,包括GET端点。
我们应该用复数名词来命名集合。我们不经常只想得到一个项目,因此我们应该与命名保持一致,应该使用复数名词。
我们使用复数来与数据库中的内容保持一致。表通常具有多个条目,并对其进行命名以反映这一点,因此,为了与表保持一致,我们应该使用与API访问的表相同的语言。
对于/articles
端点,我们对所有端点都有复数形式,因此我们不必将其更改为复数。
处理嵌套资源的端点的路径应通过将嵌套资源附加为父资源后面的路径的名称来完成。
我们必须确保它确保我们认为嵌套资源与数据库表中的资源匹配。否则,将造成混乱。
例如,如果我们希望端点获取新闻文章的评论,则应将/comments
路径附加到路径的末尾/articles
。假设我们在数据库中拥有comments
的子项article
。
例如,我们可以使用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 = []; //通过articleId获取评论的代码 res.json(评论);});app.listen(3000,()=> console.log('服务器已启动'));
在上面的代码中,我们可以在path上使用GET方法'/articles/:articleId/comments'
。我们得到comments
由标识的文章articleId
,然后在响应中将其返回。我们'comments'
在'/articles/:articleId'
路径段之后添加,以表明它是的子资源/articles
。
这是有道理的,因为假设每个文章都有其自己的注释,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');const bodyParser = require('body-parser');const app = express();//现有用户const用户= [ {电子邮件:'abc@foo.com'}]app.use(bodyParser.json());app.post('/ users',(req,res)=> { const {email} = req.body; const userExists = users.find(u => u.email ===电子邮件); 如果(userExists){ 返回res.status(400).json({错误:'用户已经存在'}) } res.json(需求主体);});app.listen(3000,()=> console.log('服务器已启动'));
在上面的代码中,我们具有users
给定电子邮件的数组中的现有用户列表。
然后,如果我们尝试使用email
中已经存在的值提交有效负载users
,我们将获得400响应状态代码,并带有一条'User already exists'
消息,让用户知道该用户已经存在。利用这些信息,用户可以通过将电子邮件更改为不存在的内容来纠正操作。
错误代码需要附带消息,以便维护人员拥有足够的信息来解决问题,但是攻击者无法使用错误内容来进行诸如窃取信息或关闭系统之类的攻击。
每当我们的API未成功完成时,我们都应通过发送错误信息并帮助用户采取纠正措施来正常地失败。
REST API背后的数据库可能会非常庞大。有时,有太多数据,不应该一次全部返回,因为它太慢或会导致系统崩溃。因此,我们需要过滤项目的方法。
我们还需要分页数据的方式,以便一次只返回一些结果。我们不想通过尝试一次获取所有请求的数据而占用资源太长时间。
过滤和分页都通过减少服务器资源的使用来提高性能。随着数据库中积累的数据越多,这些功能就越重要。
这是一个小示例,其中API可以接受带有各种查询参数的查询字符串,以使我们可以按其字段过滤出项目:
const express = require('express');const bodyParser = require('body-parser');const app = express();//员工数据在数据库中const员工= [ {firstName:“ Jane”,lastName:“ Smith”,年龄:20}, // ... {firstName:“ John”,lastName:“ Smith”,年龄:30}, {firstName:“ Mary”,lastName:“ Green”,年龄:50},]app.use(bodyParser.json());app.get('/ employees',(req,res)=> { const {firstName,lastName,age} = req.query; 让结果= [...员工]; 如果(名字){ 结果= results.filter(r => r.firstName === firstName); } 如果(姓){ 结果= results.filter(r => r.lastName === lastName); } 如果(年龄){ 结果= results.filter(r => + r.age === + age); } res.json(结果);});app.listen(3000,()=> console.log('服务器已启动'));
在上面的代码中,我们有一个req.query
变量来获取查询参数。然后,我们使用JavaScript解构语法通过将各个查询参数解构为变量来提取属性值。最后,我们filter
对每个查询参数值进行操作,以找到要返回的项目。
完成此操作后,我们将返回results
作为响应。因此,当我们使用查询字符串向以下路径发出GET请求时:
/employees?lastName=Smith&age=30
我们得到:
[ { “ firstName”:“ John”, “ lastName”:“ Smith”, “年龄”:30 }]
作为返回的响应,因为我们通过lastName
和进行了过滤age
。
同样,我们可以接受page
query参数,并从(page - 1) * 20
到返回位置的一组条目page * 20
。
我们还可以在查询字符串中指定要排序的字段。例如,我们可以从查询字符串中获取参数,其中包含我们要为其排序数据的字段。然后,我们可以按照这些单独的字段对它们进行排序。
例如,我们可能想从URL中提取查询字符串,例如:
http://example.com/articles?sort=+author,-datepublished
其中+
表示上升和-
表示下降。因此,我们按作者姓名的字母顺序(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();让缓存= apicache.middleware;app.use(cache('5分钟'));//员工数据在数据库中const员工= [ {firstName:“ Jane”,lastName:“ Smith”,年龄:20}, // ... {firstName:“ John”,lastName:“ Smith”,年龄:30}, {firstName:“ Mary”,lastName:“ Green”,年龄:50},]app.use(bodyParser.json());app.get('/ employees',(req,res)=> { res.json(员工);});app.listen(3000,()=> console.log('服务器已启动'));
上面的代码仅引用了apicache
中间件,apicache.middleware
然后得到:
app.use(cache('5 minutes'))
将缓存应用于整个应用。例如,我们将结果缓存五分钟。我们可以根据需要进行调整
如果我们对API进行任何可能破坏客户端的更改,则应该使用不同版本的API。可以像当今大多数应用程序一样,根据语义版本(例如,表示主要版本2和第六个补丁的2.0.6)完成版本控制。
这样,我们可以逐步淘汰旧的终结点,而不必强迫所有人同时迁移到新的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员工= []; //获得员工的代码 res.json(员工);});app.get('/ v2 / employees',(req,res)=> { const员工= []; //不同的代码来招募员工 res.json(员工);});app.listen(3000,()=> console.log('服务器已启动'));
我们只需将版本号添加到端点URL路径的开头即可对其进行版本控制。
设计高质量REST API的最重要要点是遵循Web标准和约定以保持一致性。JSON,SSL / TLS和HTTP状态代码都是现代Web的标准构建块。
性能也是重要的考虑因素。我们可以通过一次不返回太多数据来增加它。另外,我们可以使用缓存,这样就不必一直查询数据。
端点的路径应一致,我们仅使用名词,因为HTTP方法指示了我们要采取的行动。嵌套资源的路径应位于父资源的路径之后。他们应该告诉我们我们正在获取或操作的内容,而无需阅读额外的文档以了解它在做什么。
评论专区