13 个设计 REST API 的最佳实践


本文总阅读量

原文

写在前面

之所以翻译这篇文章,是因为自从成为一名前端码农之后,调接口这件事情就成为了家常便饭,并且,还伴随着无数的争论与无奈。编写友好的 restful api 不论对于你的同事,还是将来作为第三方服务调用接口的用户来说,都显得至关重要。关于 restful api 本身以及设计原则,我陆陆续续也看过很多的文章和书籍,在读过原文后,感觉文中指出的 13 点最佳实践还是比较全面的且具有参考意义的,因此翻译出来分享给大家。如有错误,还望指正。

由于我一般倾向于意译,关于原文中的开头语或者一些与之无关的内容,我就省略掉了,毕竟时间是金钱,英语好并且能科学上网的朋友我建议还是看原文,以免造成理解上的误差。

1. 了解应用于 REST 之上的 HTTP 知识

如果你想要构建设计优良的 REST API,了解一些关于 HTTP 协议的基础知识是很有帮助的,毕竟磨刀不误砍材工。

在 MDN 上有很多质量不错的文档介绍 HTTP。但是,就 REST API 设计本身而言,所涉及到的 HTTP 知识要点大概包含以下几条:

  • HTTP 中包含动词(或方法): GETPOSTPUTPATCH 还有 DELETE 是最常用的。
  • REST 是面向资源的,一个资源被一个 URI 所标识,比如 /articles/
  • 端点(endpoint),一般指动词与 URI 的组合,比如 GET: /articles/
  • 一个端点可以被解释为对某种资源进行的某个动作。比如, POST: /articles 可能代表“创建一个新的 article”。
  • 在业务领域,我们常常可以将动词CRUD(增删查改)关联起来:GET 代表查,POST代表增,PUTPATCH 代表改(注: PUT 通常代表整体更新,而 PATCH 代表局部更新),而 DELETE 代表删。

当然了,你可以将 HTTP 协议中所提供的任何东西应用于 REST API 的设计之中,但以上这些是比较基础的,因此时刻将它们记在脑海中是很有必要的。

2. 不要返回纯文本

虽然返回 JSON 数据格式的数据不是 REST 架构规范强制限定的,但大多 REST API 都遵循这条准则。

但是,仅仅返回 JSON 数据格式的数据还是不够的,你还需要指定返回 body 的头部,比如 Content-Type,它的值必须指定为 application/json。这一点对于程序化客户端尤为重要(比如通过 python 的 requests 模块来与 api 进行交互)—— 这些程序是否对返回数据进行正确解码取决于这个头部。

注:通常而言,对于浏览器来说,这似乎不是问题,因为浏览器一般都自带内容嗅探机制,但为了保持一致性,还是在响应中设置这个头部比较妥当。

3. 避免在 URI 中使用动词

如果你理解了第 1 条最佳实践所传达的意思,那么你现在就会明白不要将动词放入 REST API 的 URI 中。这是因为 HTTP 的动词已经足以描述执行于资源的业务逻辑操作了。

举个例子,当你想要提供一个针对某个 article 提供 banner 图片并返回的接口时,可能会实现如下格式的接口:

1
GET: /articles/:slug/generateBanner/

这里 GET 已经说明了这个接口是在做的操作,因此,可以简化为:

1
GET: /articles/:slug/banner/

类似的,如果这个端口是要创建一个 article:

1
2
3
4
5
// 不要这么做
POST: /articles/createNewArticle/

// 这才是最佳实践
POST: /articles/

尝试用 HTTP 的动词来描述所涉及的业务逻辑操作。

4. 使用复数的名词来描述资源

一些时候,使用资源的复数形式还是单数形式确实存在一定的困扰,比如使用 /article/:id/ 更好还是使用 /articles/:id/ 更好呢?

这里我推荐使用后者。为什么呢?因为复数形式可以满足所有类型端点的需求。

单数形式的 GET /article/2/ 看起来还是不错的,但是如果是 GET /article/ 呢?你能够仅通过字面信息来区分这个接口是返回某个 article 还是多个呢?

因此,为了避免有单数命名造成的歧义性,并尽可能的保持一致性,使用复数形式,比如:

1
2
3
GET: /articles/2/
POST: /articles/
...

5. 在响应中返回错误详情

当 API 服务器处理错误时,如果能够在返回的 JSON body 中包含错误信息,对于接口调用者来说,会一定程度上帮助他们完成调试。比如对于常见的提交表单,当遇到如下错误信息时:

1
2
3
4
5
6
{
"error": "Invalid payoad.",
"detail": {
"surname": "This field is required."
}
}

接口调用者很快就是明白发生错误的原因。

6. 小心 status code

这一点可能是最重要、最重要、最重要的一点,可能也是这篇文章中,唯一你需要记住的那一点。

你可能知道,HTTP 中你可以返回带有 200 状态码的错误响应,但这是十分糟糕的。不要这么做,你应当返回与返回错误类型相一致的具有一定含义的状态码

聪明的读者可能会说,我按照第 5 点最佳实践来提供足够详细的信息,难道不行吗?当然可以,不过让我讲一个故事:

我曾经使用过一个 API,对于它返回的所有响应的状态码均是 200 OK,同时通过响应数据中的 status 字段来表示当前的请求是否成功,比如:

1
2
3
4
{
"status": "success",
"data": {}
}

所以,虽然状态码是 200 OK,但我却不能绝对确定请求是否成功,事实上,当错误发生时,这个 API 会按如下代码片段返回响应:

1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Content-Type: text/html

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

头部还是 text/html,因为它同时返回了一些 HTML 片段。

正因为这样,我不得不在检查响应状态码正确的同时,还需校验这个具有特殊含义的 status 字段的值,才可以放心的处理响应返回的 data

这种设计的一个真正坏处在于,它打破了接口与调用者之间的“信任”,因为你可能会担心这个接口对你撒谎(注:言外之意就是,由于特设的字段可能会改变,因此增加了不可靠性)。

所以,使用正确的状态码,同时仅在响应的 body 中返回错误信息,并设置正确的头部,比如:

1
2
3
4
5
6
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
"error": "Expected at least two items in list."
}

7. 保持 status code 的一致性

当你掌握了正确使用状态码之后,就应该努力使它们具有一致性。

比如,如果一个 POST 类型的端点返回 201 Created,那么所有的 POST 端点都应返回同样的状态码。这样做的好处在于,调用者无需在意端点返回的状态码取决于某种特殊条件,也就形成了一致性。如果有特殊情况,请在文档中显著地说明它们。

下面是我推荐的与动词相对应的状态码:

1
2
3
4
5
GET: 200 OK
POST: 201 Created
PUT: 200 OK
PATCH: 200 OK
DELETE: 204 No Content

https://blog.florimondmanca.com/restful-api-design-13-best-practices-to-make-your-users-happy

8. 不要嵌套资源

使用 REST API 获取资源数据,通常情况下会直接获取多个或者单个,但当我们需要获取相关联的资源时,该怎么做呢?

比如说,我们期望获取作者为某个 author 的 article 列表 —— 假设 authro 的 id=12。这里提供两种方案:

第一种方案通过在 URI 中,将嵌套的资源放在所关联的资源后边来进行描述,比如:

1
GET: /authors/12/articles/

一些人推荐这种方案的理由是,这种形式的 URI 一定程度上描述了 author 与 article 之间的一对多关系。但与此同时,结合第 4 点最佳实践,我们就不太能够分清当前端点返回的数据到底是 author 类型还是 article 类型。

这里有一篇文章,详细阐述了扁平化形式优于嵌套形式,因此一定有更好的方法,这就是下面的第二种方案:

1
GET: /articles/?author_id=12

直接将筛选 article 的逻辑抽离为 querystring 即可,这样的 URI 相比之前,更加清晰地描述了“获取所有 author(id=12) 的 article”的意思。

9. 优雅地处理尾部斜杠

一个好的 URI 中是否应当包含尾部斜杠,并不具有探讨价值,选择一种更倾向的风格并保持一致性即可,同时当客户端误用尾部斜杠时,提供重定向响应。

我再来讲我自己的一个故事。某天,我在将某个 API 端点集成到项目中,但是我总是收到 500 Internal Error 的错误,我调用的端点差不多看起来这样:

1
2
3
POST: /entities
```
调试一段时间之后,我几乎崩溃了,因为我根本不知道我哪里做错了,直到我发现服务器之所以报 500 的错误,是因为我粗心丢掉了尾部斜杠(注:这种经历人人都会遇到,我在 SF 上遇过无数次类似的问题),当我把 URI 改成:

POST: /entities/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
之后,一切正常运转。

当然,大多数的 web 框架都针对 URL 是否包含尾部斜杠,进行了优雅地处理并提供定制选项,如果可以的话,找到它并开启这项功能。

## 10. 使用 querystring 来完成筛选和分页功能
大部分情况下,一个简单的端点没有办法满足负责业务场景。

你的用户可能想要获取满足一定条件下的某些数据集合 ,同时为了保证性能,仅仅获取这个集合下的一个子集。换言之,这通常叫作**筛选**功能和**分页**功能:
* 筛选:用户可以提供额外的属性来控制返回的数据集合
* 分页:获取数据集合的子集,最简单的分页是基于分页个数的分页,它由 `page` 和 `page_size` 来决定

那么问题来了,我们如何将这两项功能与 RESTful API 结合在一起呢?

答案当然是通过 **querystring**。对于分页,很显然使用这种方式再合适不过了,比如:

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

1
但对于筛选,你可能会犯第 8 点最佳实践中所指出的问题,比如获取处于 published 状态的 article 列表:

GET: /articles/published/

1
2

除了之前提出的问题外,这里还涉及一个设计上的问题,就是 **published** 本身不是资源,它仅仅是资源的特征,类似这种特征字段,应该将它们放到 querystring 中:

GET: /articles/?published=true&page=2&page_size=20
`
更加优雅、清晰,不是吗?

11. 分清 401 和 403

当我们遇到 API 中关于安全的错误提示时,很容易混淆这两个不同类型的错误,认证授权(比如权限相关)—— 老实讲,我自己也经常搞混。

这里是我自己总结的备忘录,它阐述了我如何在实际情况下,区分它们:

  • 用户是否未提供身份验证凭据?认证是否还有效?这种类型的错误一般是未认证(401 Unauthorized)。
  • 用户经过了正常的身份验证,但没有访问资源所需的权限?这种一般是未授权(403 Forbidden

12. 巧用 202 Accepted

我发现 202 Accepted 在某些场合是 201 Created 的一个非常便捷的替代方案,这个状态码的含义是:

服务器已经接受了你的请求,但是到目前为止还未创建新的资源,但一切仍处于正常状态。

我分享两种特别适合使用 202 Accepted 状态码的业务场景:

  • 如果资源是经过位于将来一系列处理流程之后才创建的,比如当某项作业完成时
  • 如果资源已经存在,但这是理想状态,因此不应该被识别为一个错误时

13. 采用 REST API 定制化的框架

作为最后一个最佳实践,让我们来探讨这样一个问题:你如何在 API 的实施中,实践最佳实践呢?

通常的情况是这样的,你想要快速创建一个 API 以便一些服务可以互相访问彼此。Python 开发者可能马上掏出了 Flask,而 JS 开发者也不甘示弱,祭出了 Express,他们会使用实现一些简单的 routes 来处理 HTTP 请求。

但这样做的问题是,通常,web 框架并不是针对构建 REST API 服务而专门存在的,换言之,Flask 和 Express 是两个十分通用的框架,但它们并非特别适合用于构建 REST API 服务。因此,你必须采取额外的步骤来实施 API 中的最佳实践,但大多数情况下,由于懒惰或者时间紧张等因素,意味着你不会投入过多精力在这些方面 —— 然后给你的用户提供了一个古怪的 API 端点。

解决方案十分简单:工欲善其事,必先利其器,掌握并使用正确的工作才是最好的方案。在各种语言中,许多专门用于构建 REST API 服务的新框架已经出现了,它们可以帮助你在不牺牲生产力的情况下,轻松地完成工作,同时遵循最佳实践。在 Python 中,我发现的最好的 API 框架之一是 Falcon。它与 Flask 一样简单,非常高效,十分适合构建 REST API 服务。如果你更喜欢 Django 的话,使用 Django REST Framework就足够了,虽然框架不是那么直观(注:按我的理解应该是说不太容易上手,但是我不这么认为),但功能非常强大。在 NodeJS 中,Restify 似乎也是一个不错的选择,尽管我还没有尝试过。我强烈建议你给这些框架一个机会!它们将帮助你构建规范,优雅且设计良好的 REST API 服务。

总结

我们都应致力于让调用 API 这件事成为一种乐趣。希望本文能使你了解到在构建更好的 REST API 服务的过程中,涉及到的一些建议和技巧。对我而言,应该把这些最佳实践归结为三点,分别是良好的语义,简洁和合理性。

目录
  1. 1. 写在前面
  2. 2. 1. 了解应用于 REST 之上的 HTTP 知识
  3. 3. 2. 不要返回纯文本
  4. 4. 3. 避免在 URI 中使用动词
  5. 5. 4. 使用复数的名词来描述资源
  6. 6. 5. 在响应中返回错误详情
  7. 7. 6. 小心 status code
  8. 8. 7. 保持 status code 的一致性
  9. 9. 8. 不要嵌套资源
  10. 10. 9. 优雅地处理尾部斜杠
  11. 11. 11. 分清 401 和 403
  12. 12. 12. 巧用 202 Accepted
  13. 13. 13. 采用 REST API 定制化的框架
  14. 14. 总结