【译】API架构 - 为REST APIs设计最佳的实践

249 阅读13分钟

REST APIs 无处不在

通常, web服务存在的时间和http协议存在的时间一样长。但是,自从云计算的出现,他们已经变成了无处不在的,可以让服务和数据可视化交互的方法。

作为一名开发者,我很幸运的能够使用一些围绕SOAP相关服务的工作(SOAP 是基于 XML 的简易协议,可使应用程序在 HTTP 之上进行信息交换),但是我主要使用REST,这是一种适用于用户开发API和Web服务的基于资源的架构风格

在我职业生涯的大部分时间里,我都参与了构建、设计和使用API的项目。

我看到过的大多数API都“声称”是RESTful风格,这意味这它们符合REST的架构原则和约束。

然而,我合作过的部分同行,写出了很槽糕的REST风格代表。

HTTP 状态码的不准确使用,纯文本返回,不一致的模式,动词的插入,我想我看到了几乎所有的问题(至少是很大一部分)

因此,我决定写一篇文章来描述,我个人关于我设计REST API的一些最佳的实践

现在,让我们来弄清楚

我不会声称自己是权威,或者说跟着我的实践是百分之百正确的,遵循这些事神圣的REST原则(如果他真的存在)。我下面讲的这些内容来自我自己的实践经验和职业生涯处理的不同API的想法

当然,我也不会假装自己已经精通了REST Api的设计,我相信这是一门艺术或一个过程,你实践的越多,掌握的越好。 我将会列举一些代码片段作为“糟糕的设计示例”, 如果看起来和你写的一样,那很好。唯一重要的事情就是,我们可以一起学习。

这里有一些技巧、建议和指导,来帮助你设计优秀REST API,这些将帮助你让你的合作者满意

1. 学习http基础

如果你追求好的REST API设计,你必须要了解基础的HTTP协议知识,我确信这可以帮助你实现更好的设计选择

我发现在Mozilla Developer Network文档中的【HTTP概述】关于HTTP讲述的很全面

尽管,对于REST API设计而言,这是一个适用于RESTful设计的HTTP TLDR

  • HTTP包括一些常见的请求方法:GET, POST, PUT, PATCH and DELETE
  • REST是面向资源,资源由URI表示:/library
  • 一个接口,由请求方法和一个URI组成,例如:Get: /books/
  • 一个接口可以解释为对资源的操作:例如POST: /books/,可能意味着创建一本新书
  • 更高级的用法,请求方法对应增删改查操作,Get代表Read,POST代表Create,Put和Patch代表update ,DELETE 代表Delete
  • 返回的状态通过状态码代表不同的含义, 1XX 代表信息, 2XX 代表成功,3XX 代表重定向,4XX 代表客户端请求错误,5XX 代表服务器出错。

当然,你可以使用http协议提供的其他内容设计REST API,但是这些基础的知识,我相信是必须要记住的内容。

2. 不要返回纯文本

尽管这不是REST架构风格强制规定的,但大多数REST API习惯返回JSON格式的数据

然而,仅仅返回包含JSON格式的内容是不够的,你应该添加header参数Content-Type,该字段必须设置成application/json,这在处理一些客户端请求或者服务请求的时候很重要(例如,其他的API服务和你的API通过Python库进行交互),他们会依赖这些header参数对返回结果进行解码

💡专业提示: 你可以用火狐浏览器很简单的验证一个返回头字段Content-Type,它们有内置的工具展示返回头信息,Content-Type: application/json🔥

1.png

在火狐中,Content-Type: text/plain看起来是这样的。

2.png

Content-Type: 'application/json看起来就很漂亮了。

3. 不要在URI中使用动词

到现在为止,如果你已经理解了这些基础,你将会意识到把动词放在URI中,这不是RESTful。

这是因为HTTP的方法已经足够精准的表达了对资源要做的操作

例如: 假如说你提供了一个接口来生成和检索一本书的封面,我将会在URL中使用:param占位符来作为一个路由参数,你的第一反应肯定是创建一个类似下面的接口:

GET: /books/:slug/generateBookCover/

但是,Get方法已经清楚的说明,我们想要检索一本书的封面,因此,我们仅仅这样写就够了

GET: /books/:slug/bookCover/

像其他的,我们创建一个新增一本书的接口

# Don’t do this
POST: /books/createNewBook/

# Do this
POST: /books/

4. 对资源使用复数名词

这是个很难的决定,是否使用或者不使用资源的复数或单数形式

我们应该使用/book/:id(单数)还是/books/:id(复数)?

我的个人建议是使用复数形式

为什么呢? 因为他更适合所有类型的接口

我看到GET /book/2/ 很好,但是 GET /book/呢? 我们是要从图书库里拿到唯一的一本书,两本书还是全部呢?

为防止出现歧义,让我们保持一致(💡软件职业的建议)并且都使用复数形式

GET: /books/2/
POST: /books/
...

5. 在返回body中展示详细的错误信息

当API服务处理出现错误时,推荐在返回body的json正文中返回详细的错误信息,来帮助你的使用者更好的调试,如果能返回具体的错误字段就更好了!

{
    "error": "Invalid payload.",
    "detail": {
        "name": "This field is required."
    }
}

6. 给HTTP状态码定义特别的提示

我觉得这一条相当重要,如果这篇文章有一条你需要记住的,那大概就是这条

最糟糕的事情就是,你的API返回一个错误的响应但状态码却是200

这简直就是糟糕的语法,取而代之的就是,返回一个有意义的HTTP状态码可以精确的描述错误的类型

然而,你可能还会存在疑问:“但是像我推荐的在body中返回一个详细的错误信息,这有什么问题呢?”

让我来讲一个故事 🙂

我曾经不得不写一个在任何情况下都返回状态码200 OK的API, 并且不管请求是否成功都通过status字段返回

{
    "status": "success",
    "data": {}
}

尽管HTTP状态码返回200 OK, 我不能确定请求的过程中是否会失败。

实际上,API返回的结果像这样:

HTTP/1.1 200 OK
Content-Type: text/html

{
    "status": "failure",
    "data": {
        "error": "Expected at least three items in the list."
    }
}

(是的,我返回了html内容,为什么不呢?)

结果,在我读取Data字段之前,我不得不同时检查状态码和status字段来完全确认返回的结果是否正确,

太烦人了🤦

这种设计是绝对不允许的,他打破了API和其调用者之间的信任,你会担心API返回的状态码欺骗了你

所有这些极大的破坏了RESTful,那我们应该怎么做呢?

确保使用HTTP状态码,仅使用body返回值来提供错误的详情

HTTP/1.1 400 Bad Request
Content-Type: application/json{
    "error": "Expected at least three items in the list."
}

7. 你的HTTP状态码应该保持不变

一旦你掌握了HTTP状态码,你应该致力于始终如一的保持不变

例如,如果你选择了一个POST口返回201来新增,那么之后在其他的地方全部使用201作为新增

为什么呢?因为调用方不用担心,哪些接口在哪些场景下返回哪些状态码

因此,保持可预测性(保持稳定),如果你不得不打破这种约定,请记录在文档上,并且做个明显的标记

通常,我都用下面这些规则

GET: 200 OK
PUT: 200 OK
POST: 201 Created
PATCH: 200 OK
DELETE: 204 No Content

8. 不要嵌套资源

你现在可能已经注意到当前REST API处理资源,检索一个列表或者直接检索单个资源的实例。但是,如果处理关联的资源该怎么办呢?

例如,假如说我们想要检索特定作者的书籍列表,带有参数name=cagna, 它有两个基础选项。

第一个选项就是将资源嵌套在books资源下的authors,例如

GET: /authors/Cagan/books/

一些架构师推荐这些约定,因为它确实代表了作者和其书籍之间,一对多的关系

但是,这是不清晰的,你请求的资源类型是什么?作者还是书籍呢?

同样扁平比嵌套要更好,因此这里可以有更好的方法。

我个人推荐直接使用query参数去过滤书籍资源

GET: /books?author=Cagan

这种写法清晰的表示,获取作者名字为Cagan的所有的书籍,对吧?🙂

9. 优雅的处理尾部斜杠

URIs 后面是否携带一个尾部的斜杠并不是一个需要争论的事情。 你可以选择其中的一种方式:带斜杠或者不带斜杠,坚持做下去,并且如果他们没有按照你的约定,也可以做个重定向。

(我承认,我已经为不止一次犯过这个错误。🙈)

故事时间!📙有一天,我为一个项目写了API,然后调用的时候,每次返回都是500错误,调用的时候,我的请求是这样的:

POST: /buckets

我很困惑,我一生的经验都不能指出我到底哪里出错了。🤪

最后,结果服务请求失败,是因为我没有给url加尾部的斜杠,因此我修改了请求方式:

POST: /buckets/

这样,一切都正常了。🤦‍♂️

API没有被修复,但是希望你可以避免这种让调用者费解的问题发生

💡工作提示: 大多数web框架(Angular、React等)都有关于尾部斜杠重定向的配置,找到配置的地方,尽早让配置生效。

10. 确保使用query参数做筛选和分页

大多数时间,一个简单的接口设计不能满足于各种各样复杂的业务场景

你的API使用者可能会想要检索特定条件下的所有资源,或者为了提高性能,检索大量数据中的一部分。

这个需求完全就是筛选和分页的目的

筛选是调用者通过传递特殊的参数,返回他所需要的数据

分页允许所有的调用者检索数据集的一小部分。最简单的分页方式是页码分页,就是通过参数pagepage_size检索。

现在,问题是:你如何通过这些参数用REST API实现这个功能?

我的答案就是:使用查询参数

我会说很明显为什么说你应该使用query参数来做分页,看起来就像下面的实现:

GET: /books?page=1&page_size=10

但是,他可能缺少过滤条件,首先,你可能会想实现这样的需求,仅仅检索一部分已经出版的书籍,实现如下:

GET: /books/published/

设计的缺陷:发布的不是一个资源,他只是你检索数据的一个特征。这个条件可以放在query参数中

因此,最后实现一个检索:筛选20条数据中出版过的书籍中的后十条,像下面这种方式

GET: /books?published=true&page=2&page_size=10

很清晰,不是吗?

11. 知道401 Unauthorized(未认证)和403Forbidden(禁止访问的区别)

每次我都有四分之一的时间,看到一些开发者,甚至一些有经验的架构师对这两者的认识都很混乱

当处理REST API的安全错误时,常常很容易混淆错误是与身份认证还是授权(权限)问题有关

下面是我的备忘清单,告诉我如何处理这种情况,具体依赖于对应的场景

  • 如果调用者没有提供身份认证信息,不管是SSO Token不合法或者超时 都是👉401 Unauthorized
  • 当调用者身份认证通过,但是没有资源的访问权限,就返回👉 403 Forbidden

12. 更好的使用HTTP202 Accepted

我发现202 Accepted状态码很容易替代201 Created,它表示

我,服务器,已经理解了你的请求,但是我还没有创建资源,但是程序正常

我发现这里有两个主要的场景,特别适合的202 Accepted状态码

  • 如果资源作为下一个进程执行的结果,将会被创建,例如在一个任务或进程完成后。
  • 如果资源已经以某种方式存在,这个就不应该被解释为错误

13. 使用专门用于REST API的Web框架

作为最后一个最佳实践,让我们来讨论这个问题:如何让你在API设计中使用最佳实现?

大多数实践,你想要创建一个更快的api,以方便让服务可以更好的交互

Python开发者倾向于使用Flask框架,JavaScript开发者喜欢用Node(Express),它们可以实现一些简单的路由来处理HTTP请求

这个方法的问题是,通常框架的目标不是致力于构建Rest API服务

例如,Flask和Express 是两个很通用的框架,但是它们不会特意帮助我们设计构建REST API

结果,你不得不花费额外的工作来构建API,并且大多数时间,懒惰和没有时间导致你不会在API的设计上努力—— 最后留给你的调用者一些奇奇怪怪的API

解决方法很简单,在工作中使用合适的工具

新的框架已经出现在不同的语言中,可以帮助构建REST API,它们帮助你遵循一站式的最佳实践,并且是在不牺牲生产效率的情况下。

在Python中,我发现最好的一个API框架就是Falcon,他像Flask一样使用简单,高效,可以在几分钟内构建REST API。

Falcon:减轻我们 0.0564 多个世纪以来 API 的负担。

如果你更多的使用Django,可以使用Django REST Framework框架,他不是很直观,但是却很强大

Node的用户,Restify似乎是一个很好的选择,尽管我还没有来得及尝试

我强烈的推荐你尝试下这些框架,它们可以帮助你构建更好的,优雅的REST API

结论📕

我们都应该努力让API使用起来很愉快,包括调用者和我们的依赖者

我希望这篇文章可以给你一些提示,并且提供一些启发,帮助你更好的构建api, 对我来说,这仅仅归结为良好的语义、简单性和常识

REST API设计是一门艺术,比什么都重要

如果你对我上面的分享有任何不同的方法,请分享出来,我将会认真倾听。

同时,让他们继续精进API的设计! 💻