架构必备「RESTful API」设计技巧经验总结

简介: 【译者注】本文是作者在自己的工作经验中总结出来的RESTful API设计技巧,虽然部分技巧仍有争议,但总体来说还是有一定的参考价值的。以下是译文。

【译者注】本文是作者在自己的工作经验中总结出来的RESTful API设计技巧,虽然部分技巧仍有争议,但总体来说还是有一定的参考价值的。以下是译文。


简单说一下代码重用


记得在Ken Rogers的Medium博客里曾经见过这么一句话(原文出自海明威):


我们都是手艺学徒,没有人会成为大师。


在我写这篇文章的时候,我不禁笑了起来,因为从这件事情的背后看到了一个伟大的类比,那就是从其他人那里引用了海明威的话。也就是说,我不需要为了得到类似的功能和结果而花费精力自己去创建一个与众不同的东西,上面提到的海明威的话正是代码重用在文学上的例子。


但是,我在这里不会写代码包的好处,而是更多地提一些我的感受,这些感受会在当前以及未来的项目中积极地得到实现。我还总结了一套API规则和原语,包括了功能和实现细节。


使用API版本控制


如果你要开发一个提供客户端服务的API,你需要为最后可能的修改而做好准备。最好的办法就是通过为RESTful API提供“版本命名空间”来实现。

我们只需将版本号作为前缀添加到所有的URL里即可。

GET www.myservice.com/api/v1/posts

然而,在我研究了其他的API实现之后发现,我喜欢上了这种较短的URL样式,它把api作为是子域名的一部分,并从路由中删除了/api,这样更短、更简洁。

GET api.myservice.com/v1/posts

跨域资源共享(CORS)


需要重点关注的是,如果你打算在www.myservice.com上托管你的前端站点,而将API放在另外一个不同的子域上,例如api.myservice.com,那么你需要在后端实现CORS,这样才能使得AJAX调用不会抛出 No Access-Control-Allow-Origin header is present 这样的错误。


使用复数形式


当你从/posts请求多个帖子的时候,这样的URL看起来更明了:

// 复数形式看起来更一致,更有意义
GET /v1/posts/:id/attachments/:id/comments
// 不能有歧义
// 这只是一个评论? 还是一个表格?
GET /v1/post/:id/attachment/:id/comment

更多有关混合类型的信息,请看下文:“使用根级别的‘me’端点(URL)”。

避免查询字符串

查询字符串的作用是对关系数据库返回的记录集做进一步地过滤。

/projects/:id/collections 优于 /collections?projectId=:id。
/projects/:id/collections/:id/items 优于 /items?projectId=:id&collectionId=:id。

更多信息请看下文:“避免对嵌套路由的操作”。


使用HTTP方法


我们可使用下面这些HTTP方法:


GET 用于获取数据。


POST 用于添加数据。


PUT 用于更新数据(整个对象)。


PATCH 用于更新数据(附带对象的部分信息)。


DELETE 用于删除数据。


补充一点,对于修改对象的部分内容的请求来说,我认为PATCH是减少请求包大小的一个好的方法,并且它也能很好的跟自动提交/自动保存字段配合起来用。


一个很好的例子是Tumblr的“仪表盘设置”屏幕,其中,“服务的用户体验”的一些非关键性选项可以单独地编辑和保存,而不需要点最下面的提交按钮。


对于POST,PUT或PATCH的成功响应消息,应该返回更新后的对象,而不是只返回一个null。点击这里有一篇http1.0和2.0的对比。


有关响应的其他内容,请阅读下文:“JSON格式的响应和请求”。


使用封包


“我不喜欢数据封包。它只是引入了另一个键来浏览数据树。元信息应该包含在包头中。”


最初,我坚持认为封包数据是不必要的,HTTP协议已经提供了足够的“封包”来传递响应消息。


然而,根据Reddit上的回复所述,如果不封包为JSON数组,则可能会出现各种漏洞和潜在的黑客攻击。


现在建议使用封包,你应该把数据封包后再应答!

// 已封包,最顶级的对象既安全又简洁
{
  data: [
    { ... },
    { ... },
    // ...
  ]
}
// 未封包,存在安全风险
[
  { ... },
  { ... },
  // ...
]

同样要重点关注的是,不像其他语言那样,JavaScript之类的语言将会将空对象认为是true! 因此,在下面这种情况下,不要返回空的对象来作为响应的一部分:

// 从payload中提取封包和错误
const { data, error } = payload
// 错误处理
if (error) { throw ... }
// 否则
const normalizedData = normalize(data, schema)

JSON格式的响应和请求


所有东西都应该被序列化成JSON。如果你期待从服务器上获取JSON格式的数据,那么请客气一点,请发送JSON格式的内容给服务器。请两边保持一致!


某些情况下,如果动作执行成功(例如DELETE),那我并没有什么需要返回的。但是,在某些语言(如Python)中返回一个空对象可能被认为是false,并且在开发人员调试程序的时候,这种情况并不容易发现。因此,我喜欢返回“OK”,尽管这是一个字符串,但是在返回的时候会被包装成一个简单的响应对象。

DELETE /v1/posts/:id
// response - HTTP 200
{
  "message": "OK"
}

使用HTTP状态码和错误响应


因为我们使用了HTTP方法,所以我们应当使用HTTP状态码。


我喜欢使用这些状态码:


对于数据错误

400:请求信息不完整或无法解析。 422:请求信息完整,但无效。 404:资源不存在。 409:资源冲突。


####


对于鉴权错误

401:访问令牌没有提供,或者无效。 403:访问令牌有效,但没有权限。


####


对于标准状态

200: 所有的都正确。 500: 服务器内部抛出错误。


假设要创建一个新帐户,我们提供了email和password两个值。我们希望让客户端应用程序能够阻止任何无效的电子邮件或密码太短的请求,但外部人员可以像我们的客户端应用程序一样在需要的时候直接访问API。


如果email字段丢失,则返回400。 如果password字段太短,则返回422。 如果email字段不是有效的电子邮件,则返回422。 如果email已经被使用,返回一个409。


从上面这些情况来看,有两个错误会返回422,不过他们的原因是不同的。这就是为什么我们需要一个错误码,甚至是一个错误描述。要区分代码和描述,我打算将error(代码)作为机器可识别的常量,将description作为可更改的用于人类识别的字符串。点击这里有一篇http1.0和2.0的对比。


字段校验错误

对于字段的错误,可以这样返回:

POST /v1/register
// 请求
{
  "email": "end@@user.comx"
  "password": "abc"
}
// 响应 - 422
{
  "error": {
    "status": 422,
    "error": "FIELDS_VALIDATION_ERROR",
    "description": "One or more fields raised validation errors."
    "fields": {
      "email": "Invalid email address.",
      "password": "Password too short."
    }
  }
}

操作校验错误

对于返回操作校验错误:

POST /v1/register
// 请求
{
  "email": "end@user.com",
  "password": "password"
}
// 响应 - 409
{
  "error": {
    "status": 409,
    "error": "EMAIL_ALREADY_EXISTS",
    "description": "An account already exists with this email."
  }
}

这样,你的程序的错误提取逻辑要当心非200的错误了,你可以直接从响应中检查error字段,然后将其与客户端中相应的逻辑进行比较。


status这个字段似乎也很有用,如果你不想检查响应里的元数据,那你可以在需要的时候有条件地添加这个字段。


description可作为备用的用户可读的错误消息。


密码规则


在做了很多密码规则的研究之后,我比较赞同《密码规则是废话》(https://blog.codinghorror.com/password-rules-are-bullshit/)和《NIST禁止做的事情》(https://nakedsecurity.sophos.com/2016/08/18/nists-new-password-rules-what-you-need-to-know/)这两篇帖子的观点。


整理了一些处理密码的规则:


1. 执行unicode密码的最小长度策略(最小8-10位)。


2. 检查常见的密码(例如“password12345”)


3. 检查密码熵(不允许使用“aaaaaaaaaaaaa”)。


4. 不要使用密码编写规则(至少包含其中一个字符“!@#$%&”)。


5. 不要使用密码提示(“assword”这样的)。


6. 不要使用基于知识的认证。


7. 不要超期不修改密码。


8. 不要使用短信进行双认证。


9. 使用32位以上的密码盐(salt)。


在某种程度上,所有这些规则能使密码验证更容易!


使用访问和刷新令牌


现代的无状态、RESTful API一般会使用令牌来实现身份认证。这消除了在无状态服务器上处理会话和Cookie的需要,并且可以很容易地使用Authorization头(或access_token查询参数)来调试网络请求。点击这里有一篇JWT生成token实战。


访问令牌用于认证所有未来的API请求,生命期短,不会被取消。


刷新令牌在初始登录的响应中返回,然后跟过期时间戳和与使用者的关系一起进行散列计算后存储到数据库中。这个长生命期的像密码一样的密钥,可以被用来请求新的短生命期的JWT访问令牌。刷新令牌也可以用于续订并延长其使用寿命,这意味着如果用户持续使用该服务,则无需再次登录。


但是,如果API希望签订一个不同的“密钥”,JWT就会被取消,但是这将使所有当前发出的令牌全部无效,但因为这些令牌是短生命期的,所以这并没有关系。


登录

在我的程序实现中,正常的登录过程如下所示:


1. 通过/login接收邮件和密码。


2. 检查数据库的电子邮件和密码哈希。


3. 创建一个新的刷新令牌和JWT访问令牌。


4. 返回以上两个数据。


续订令牌

正常的续订验证流程如下所示:


1. 尝试从客户端创建请求时,JWT已经过期。


2. 将刷新令牌提交到/renew。


3. 通过将刷新令牌进行哈希与数据库中保存的进行匹配。


4. 成功后,创建新的JWT访问令牌并延长到期时间。


5. 返回访问令牌。


验证令牌

通过检查到期日期和签名哈希可以校验JWT访问令牌的有效性。如果校验失败,则认为是一个无效的令牌。


如果验证通过,则JWT的有效载荷中包含了一个uid,它用于在API响应的上下文中传递一个对应的user对象来检查权限/角色,并相应地创建/读取/更新/删除数据。


终止会话

由于刷新令牌存储在数据库中,因此可以将其删除来“终止会话”。这为用户提供了一个控制方法,即他们可以通过主动的刷新令牌“会话”来保护自己的帐户,并且通过这种方法来进行多次重复认证(通过调整超时时间戳来实现)。


让JWT保持小巧


在把信息序列化到JWT访问令牌中时,请尽可能地让这个信息小巧,身份验证令牌的生命期不需要很长,因此没必要。如果可以的话,只序列化用户的uid(id)就可以了,其余的可以通过“GET /me”来传递。点击这里有一篇JWT生成token实战。


还值得注意的是,存储在JWT有效载荷中的任何敏感信息并不安全,因为它只是一个经过base64编码的字符串。


使用根级别的“Me”端点(URL)


一般人会使用/profile这个URL来提供自身的基本属性。但是,我也看到过比较混论的实现,例如对于/users/:id这种接受整数的URL,它竟然允许传入字符串me来指向自身的属性。


通过/me访问自身信息的更深层次的URL,例如/me的/settings或者/billing信息,而通过users/:id/billing访问其他用户的信息。

// 不推荐
GET /v1/users/me
// 推荐,因为更短,没有把整数和字符串混在一起
GET /v1/me

避免对嵌套路由的操作

有一个采用了以上一些设计理念的重构的项目,最后却设计出了一个难用的URL系统:

// 一个长长的URL
PATCH /v1/projects/:id/collections/:id/items/:id/attachments

如果要POST上传一个附件,这个URL可能看起来还行,但是如果在开发客户端应用程序时想要实现像对附件标星号这么一个简单操作的功能的话,那你就需要重写相关的代码。相关代码如下:

const apiRoot = 'https://api.myservice.com/v1'
const starAttachment = (projectId, collectionId, itemId, attachmentId, starred) => {
  fetch(
    `${apiRoot}/projects/${projectId}/collections/${collectionId}/items/${itemId}/attachments/${attachmentId}`,
    {
      method: 'PATCH',
      body: JSON.stringify({ starred }),
      // ...
    }
}

attachments.js

助手函数的代码如下:

import { starAttachment } from './actions/attachments.js'
class MyComponent extends React.Component {
  doStarAttachment = (id, starred) => {
    // now all the "boilerplate" for starring the attachment
    const {
      projectId,
      collectionsId,
      itemId
    } = this.props.entities.attachments[id]
    // now actually plugging in all that information
    starAttachment(projectId, collectionId, itemId, id, starred)
  }
  // ...
}

MyComponent.js

如果你把获取附件属性这个功能委派给服务器来实现,并且只使用根级别的URL,这样不是更好吗?

const apiRoot = 'https://api.myservice.com/v1'
const starAttachment = (id, starred) => {
  fetch(
    `${apiRoot}/attachments/${id}`,
    {
      method: 'PATCH',
      body: JSON.stringify({ starred }),
      // ...
    }
}

attachments.js

import { starAttachment } from './actions/attachments.js'
class MyComponent extends React.Component {
  doStarAttachment = (id, starred) => {
    // simple as, and you could even easily call it from a gallery-like list
    starAttachment(id, starred)
  }
  // ...
}

MyComponent.js


总的来说,我认为这两种方法各有各的优势,而我倾向于用一个长的路径来创建/提取资源,用一个短的路径来更新/删除资源。


提供分页功能


分页很重要,因为你不会想让一个简单的请求就获得数千行的记录。这个问题似乎很明显,但是还是会有许多人忽略这个功能。


有多种方法来实现分页:


“From”参数

可以说这是最容易实现的,API接受一个from查询字符串参数,然后从这个偏移量开始返回有限数量的结果(通常返回20个结果)。


另外最好提供一个limit参数来限制最大记录数,例如Twitter,最大限制为1000,而默认限制为200。


“下一页”令牌

如果每页20个结果之外还有其他的结果,谷歌的Places API就会在响应中返回next_page_token。然后,服务器在新的请求中接收到这个令牌后,就会返回更多的结果,并附带新的next_page_token,直到所有的结果全部都返回给客户端。


Twitter使用参数next_cursor实现了类似的功能。


实现“健康检查”URL


很有必要提供一种方法来输出一个简单的响应,以此来表明API实例是活着的,不需要重新启动。这个功能也很有用,通过它可以很方便地检查某个时间点的某台服务器上的API是什么版本,而这无需通过认证。

GET /v1
// response - HTTP 200
{
  "status": "running",
  "version": "fdb1d5e"
}

我提供了status和version这两个值。另外值得一提的是,这个值是从version.txt文件读取到的,如果读取错误或者文件不存在,则默认值为 :__UNKNOWN__

原文:https://medium.com/@petertboyer/learn-restful-api-design-ideals-c5ec915a430f 作者:Peter Boyer 翻译:雁惊寒

相关文章
|
2月前
|
JSON 缓存 JavaScript
深入浅出:使用Node.js构建RESTful API
在这个数字时代,API已成为软件开发的基石之一。本文旨在引导初学者通过Node.js和Express框架快速搭建一个功能完备的RESTful API。我们将从零开始,逐步深入,不仅涉及代码编写,还包括设计原则、最佳实践及调试技巧。无论你是初探后端开发,还是希望扩展你的技术栈,这篇文章都将是你的理想指南。
|
1月前
|
JSON JavaScript 前端开发
深入浅出Node.js:从零开始构建RESTful API
在数字化时代的浪潮中,后端开发作为连接用户与数据的桥梁,扮演着至关重要的角色。本文将引导您步入Node.js的奇妙世界,通过实践操作,掌握如何使用这一强大的JavaScript运行时环境构建高效、可扩展的RESTful API。我们将一同探索Express框架的使用,学习如何设计API端点,处理数据请求,并实现身份验证机制,最终部署我们的成果到云服务器上。无论您是初学者还是有一定基础的开发者,这篇文章都将为您打开一扇通往后端开发深层知识的大门。
56 12
|
1月前
|
设计模式 负载均衡 监控
探索微服务架构下的API网关设计
在微服务的大潮中,API网关如同一座桥梁,连接着服务的提供者与消费者。本文将深入探讨API网关的核心功能、设计原则及实现策略,旨在为读者揭示如何构建一个高效、可靠的API网关。通过分析API网关在微服务架构中的作用和挑战,我们将了解到,一个优秀的API网关不仅要处理服务路由、负载均衡、认证授权等基础问题,还需考虑如何提升系统的可扩展性、安全性和可维护性。文章最后将提供实用的代码示例,帮助读者更好地理解和应用API网关的设计概念。
76 8
|
2月前
|
XML JSON 缓存
深入理解RESTful API设计原则与实践
在现代软件开发中,构建高效、可扩展的应用程序接口(API)是至关重要的。本文旨在探讨RESTful API的核心设计理念,包括其基于HTTP协议的特性,以及如何在实际应用中遵循这些原则来优化API设计。我们将通过具体示例和最佳实践,展示如何创建易于理解、维护且性能优良的RESTful服务,从而提升前后端分离架构下的开发效率和用户体验。
|
2月前
|
监控 安全 API
深入浅出:构建高效RESTful API的最佳实践
在数字化时代,API已成为连接不同软件和服务的桥梁。本文将带你深入了解如何设计和维护一个高效、可扩展且安全的RESTful API。我们将从基础概念出发,逐步深入到高级技巧,让你能够掌握创建优质API的关键要素。无论你是初学者还是有经验的开发者,这篇文章都将为你提供实用的指导和启示。让我们一起探索API设计的奥秘,打造出色的后端服务吧!
|
2月前
|
JSON 缓存 测试技术
构建高效RESTful API的后端实践指南####
本文将深入探讨如何设计并实现一个高效、可扩展且易于维护的RESTful API。不同于传统的摘要概述,本节将直接以行动指南的形式,列出构建RESTful API时必须遵循的核心原则与最佳实践,旨在为开发者提供一套直接可行的实施框架,快速提升API设计与开发能力。 ####
|
2月前
|
JavaScript NoSQL API
深入浅出Node.js:从零开始构建RESTful API
在数字化时代的浪潮中,后端开发如同一座灯塔,指引着数据的海洋。本文将带你航行在Node.js的海域,探索如何从一张白纸到完成一个功能完备的RESTful API。我们将一起学习如何搭建开发环境、设计API结构、处理数据请求与响应,以及实现数据库交互。准备好了吗?启航吧!
|
2月前
|
JSON API 数据格式
探索后端开发:从零构建简易RESTful API
在数字时代的浪潮中,后端开发如同搭建一座桥梁,连接着用户界面与数据世界。本文将引导读者步入后端开发的殿堂,通过构建一个简易的RESTful API,揭示其背后的逻辑与魅力。我们将从基础概念出发,逐步深入到实际操作,不仅分享代码示例,更探讨如何思考和解决问题,让每一位读者都能在后端开发的道路上迈出坚实的一步。
|
2月前
|
JSON API 开发者
深入理解RESTful API设计原则
在数字化时代,API已成为连接不同软件应用的桥梁。本文旨在探讨RESTful API设计的基本原则和最佳实践,帮助开发者构建高效、可扩展的网络服务接口。通过解析REST架构风格的核心概念,我们将了解如何设计易于理解和使用的API,同时保证其性能和安全性。
|
2月前
|
存储 缓存 API
深入理解RESTful API设计原则
在现代软件开发中,RESTful API已成为前后端分离架构下不可或缺的通信桥梁。本文旨在探讨RESTful API的核心设计原则,包括资源导向、无状态、统一接口、以及可缓存性等,并通过实例解析如何在实际应用中遵循这些原则来构建高效、可维护的API接口。我们将深入分析每个原则背后的设计理念,提供最佳实践指导,帮助开发者优化API设计,提升系统整体性能和用户体验。
42 0

热门文章

最新文章