Iawen's Blog

我喜欢这样自由的随手涂鸦, 因为我喜欢风......

RESTful 是目前最流行的 API 设计规范, 用于 Web 数据接口的设计。
它的大原则容易把握, 但是细节不容易做对。本文总结 RESTful 的设计细节, 介绍如何设计出易于理解和使用的 API。

一. URL 设计

1.1 动词 + 宾语

RESTful 的核心思想就是, 客户端发出的数据操作指令都是"动词 + 宾语"的结构。比如, GET /articles这个命令, GET是动词, /articles是宾语。
动词通常就是五种 HTTP 方法, 对应 CRUD 操作。

  • GET: 读取(Read)
  • POST: 新建(Create)
  • PUT: 更新(Update)
  • PATCH: 更新(Update), 通常是部分更新
  • DELETE: 删除(Delete)
    根据 HTTP 规范, 动词一律大写。

1.2 动词的覆盖

有些客户端只能使用 GET和 POST这两种方法。服务器必须接受 POST模拟其他三个方法( PUT、 PATCH、 DELETE)。
这时, 客户端发出的 HTTP 请求, 要加上 X-HTTP-Method-Override属性, 告诉服务器应该使用哪一个动词, 覆盖 POST方法。

POST /api/Person/4 HTTP/1.1
X-HTTP-Method-Override: PUT

上面代码中, X-HTTP-Method-Override指定本次请求的方法是 PUT, 而不是 POST。

1.3 宾语必须是名词

宾语就是 API 的 URL, 是 HTTP 动词作用的对象。它应该是名词, 不能是动词。比如, /articles这个 URL 就是正确的, 而下面的 URL 不是名词, 所以都是错误的。

/getAllCars
/createNewCar
/deleteAllRedCars

1.4 复数 URL

既然 URL 是名词, 那么应该使用复数, 还是单数?
这没有统一的规定, 但是常见的操作是读取一个集合, 比如 GET /articles(读取所有文章), 这里明显应该是复数。
为了统一起见, 建议都使用复数 URL, 比如 GET /articles/2要好于 GET/article/2

1.5 避免多级 URL

常见的情况是, 资源需要多级分类, 因此很容易写出多级的 URL, 比如获取某个作者的某一类文章。

GET /authors/12/categories/2

这种 URL 不利于扩展, 语义也不明确, 往往要想一会, 才能明白含义。
更好的做法是, 除了第一级, 其他级别都用查询字符串表达。

GET /authors/12?categories=2

下面是另一个例子, 查询已发布的文章。你可能会设计成下面的 URL。

GET /articles/published

# 查询字符串的写法明显更好。
GET /articles?published=true

二. 状态码

2.1 状态码必须精确

客户端的每一次请求, 服务器都必须给出回应。回应包括 HTTP 状态码和数据两部分。
HTTP 状态码就是一个三位数, 分成五个类别。

  • 1xx: 相关信息
  • 2xx: 操作成功
  • 3xx: 重定向
  • 4xx: 客户端错误
  • 5xx: 服务器错误
    这五大类总共包含100多种状态码, 覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释, 客户端只需查看状态码, 就可以判断出发生了什么情况, 所以服务器应该返回尽可能精确的状态码。
    API 不需要 1xx状态码, 下面介绍其他四类状态码的精确含义。

2.2 2xx 状态码

200状态码表示操作成功, 但是不同的方法可以返回更精确的状态码。

  • GET: 200 OK
  • POST: 201 Created
  • PUT: 200 OK
  • PATCH: 200 OK
  • DELETE: 204 No Content
    上面代码中, POST返回 201状态码, 表示生成了新的资源; DELETE返回 204状态码, 表示资源已经不存在。
    此外, 202 Accepted状态码表示服务器已经收到请求, 但还未进行处理, 会在未来再处理, 通常用于异步操作。下面是一个例子。
HTTP/1.1202
Accepted
{
    "task":
    {
        "href":"/api/company/job-management/jobs/2130040",
        "id":"2130040"
    }
}

2.3 3xx 状态码

API 用不到 301状态码(永久重定向)和 302状态码(暂时重定向, 307也是这个含义), 因为它们可以由应用级别返回, 浏览器会直接跳转, API 级别可以不考虑这两种情况。
API 用到的 3xx状态码, 主要是 303 See Other, 表示参考另一个 URL。它与 302和 307的含义一样, 也是"暂时重定向", 区别在于 302和 307用于 GET请求, 而 303用于 POST、 PUT和 DELETE请求。收到 303以后, 浏览器不会自动跳转, 而会让用户自己决定下一步怎么办。下面是一个例子。

HTTP/1.1 303 See Other
Location: /api/orders/12345

2.4 4xx 状态码

4xx状态码表示客户端错误, 主要有下面几种。

  • 400 Bad Request: 服务器不理解客户端的请求, 未做任何处理。
  • 401 Unauthorized: 用户未提供身份验证凭据, 或者没有通过身份验证。
  • 403 Forbidden: 用户通过了身份验证, 但是不具有访问资源所需的权限。
  • 404 Not Found: 所请求的资源不存在, 或不可用。
  • 405 Method Not Allowed: 用户已经通过身份验证, 但是所用的 HTTP 方法不在他的权限之内。
  • 410 Gone: 所请求的资源已从这个地址转移, 不再可用。
  • 415 Unsupported Media Type: 客户端要求的返回格式不支持。比如, API 只能返回 JSON 格式, 但是客户端要求返回 XML 格式。
  • 422 Unprocessable Entity : 客户端上传的附件无法处理, 导致请求失败。
  • 429 Too Many Requests: 客户端的请求次数超过限额。

2.5 5xx 状态码

5xx状态码表示服务端错误。一般来说, API 不会向用户透露服务器的详细信息, 所以只要两个状态码就够了。

  • 500 Internal Server Error: 客户端请求有效, 服务器处理时发生了意外。
  • 503 Service Unavailable: 服务器无法处理请求, 一般用于网站维护状态。

三. 服务器回应

3.1 不要返回纯本文

API 返回的数据格式, 不应该是纯文本, 而应该是一个 JSON 对象, 因为这样才能返回标准的结构化数据。所以, 服务器回应的 HTTP 头的 Content-Type属性要设为 application/json。
客户端请求时, 也要明确告诉服务器, 可以接受 JSON 格式, 即请求的 HTTP 头的 ACCEPT属性也要设成 application/json。下面是一个例子。

GET /orders/2 HTTP/1.1
Accept: application/json

3.2 发生错误时, 不要返回 200 状态码

有一种不恰当的做法是, 即使发生错误, 也返回 200状态码, 把错误信息放在数据体里面, 就像下面这样。

HTTP/1.1200OK
Content-Type:application/json
{
    "status":"failure",
    "data":
    {
        "error":"Expectedatleasttwoitemsinlist."
    }
}

上面代码中, 解析数据体以后, 才能得知操作失败。
这张做法实际上取消了状态码, 这是完全不可取的。正确的做法是, 状态码反映发生的错误, 具体的错误信息放在数据体里面返回。下面是一个例子。

HTTP/1.1400BadRequest
Content-Type:application/json
{
    "error":"Invalidpayoad.",
    "detail":
    {
        "surname":"Thisfieldisrequired."
    }
}

3.3 提供链接

API 的使用者未必知道, URL 是怎么设计的。一个解决方法就是, 在回应中, 给出相关链接, 便于下一步操作。这样的话, 用户只要记住一个 URL, 就可以发现其他的 URL。这种方法叫做 HATEOAS。
举例来说, GitHub 的 API 都在 api.github.com 这个域名。访问它, 就可以得到其他 URL。

{
    ...
    "feeds_url":"https://api.github.com/feeds",
    "followers_url":"https://api.github.com/user/followers",
    "following_url":"https://api.github.com/user/following{/target}",
    "gists_url":"https://api.github.com/gists{/gist_id}",
    "hub_url":"https://api.github.com/hub",
    ...
}

上面的回应中, 挑一个 URL 访问, 又可以得到别的 URL。对于用户来说, 不需要记住 URL 设计, 只要从 api.github.com 一步步查找就可以了。
HATEOAS 的格式没有统一规定, 上面例子中, GitHub 将它们与其他属性放在一起。更好的做法应该是, 将相关链接与其他属性分开。

HTTP/1.1200OK
Content-Type:application/json
{
    "status":"Inprogress",
    "links":
    {[
        {
            "rel":"cancel",
            "method":"delete",
            "href":"/api/status/12345"
        },
        {
            "rel":"edit",
            "method":"put",
            "href":"/api/status/12345"
        }
    ]}
}

四. REST API URI 设计的七准则

在了解 REST API URI 设计的规则之前, 让我们快速过一下我们将要讨论的一些术语。

  • URI: REST API 使用统一资源标识符(URI)来寻址资源。在今天的网站上, URI 设计范围从可以清楚地传达API的资源模型, 如:
http://api.example.com/louvre/leonardo-da-vinci/mona-lisa

到那些难以让人理解的, 比如:

http://api.example.com/68dd0-a9d3-11e0-9f1c-0800200c9a66

Tim Berners-Lee 在他的“Web架构公理”列表中列出了关于 URI 的不透明度的注释:

唯一可以使用标识符的是对对象的引用。当你没有取消引用时, 你不应该查看 URI 字符串的内容以获取其他信息。

  • Tim Berners-Lee

客户端必须遵循 Web 的链接范例, 将 URI 视为不透明标识符。
REST API 设计人员应该创建 URI, 将 REST API 的资源模型传达给潜在的客户端开发人员。在这篇文章中, 我将尝试为 REST API URsI 引入一套设计规则。
在深入了解规则之前, 先看一下在 RFC 3986 中定义的通用 URI 语法, 如下所示:

URI = scheme "://" authority "/" path ["?" query] ["#" fragment]

规则#1: URI中不应包含尾随的斜杠(/)

这是作为 URI 路径中最后一个字符的最重要的规则之一, 正斜杠(/)不会增加语义值, 并可能导致混淆。REST API 不应该期望有一个尾部的斜杠, 并且不应该将它们包含在它们提供给客户端的链接中。
许多 Web 组件和框架将平等对待以下两个 URI:

http://api.canvas.com/shapes/
http://api.canvas.com/shapes

然而, URI 中的每个字符都会被计入作为资源的唯一标识。
两个不同的 URI 映射到两个不同的资源。如果 URI 不同, 那么资源也会不同, 反之亦然。因此, REST API 必须生成和传达清晰的 URI, 并且不应容忍任何客户端尝试去对一个资源进行模糊的标识。
更多的API可能会将客户端重定向到末尾没有斜杠的 URI 上, (他们也可能会返回 301 - 用于重新定位资源的 “Moved Permanently”)。

规则#2: 正斜杠分隔符(/)必须用于指示层次关系

在 URI 的路径部分的正斜杠(/), 用于表示资源之间的层次关系。例如:

http://api.canvas.com/shapes/polygons/quadrilaterals/squares

规则#3: 应使用连字符( - )来提高 URI 的可读性

为了使你的 URI 容易被人检索和解释, 请使用连字符( - )来提高长路径段中名称的可读性。在任何你将使用英文的空格或连字号的地方, 在URI中都应该使用连字符来替换。例如:

http://api.example.com/blogs/guy-levin/posts/this-is-my-first-post

规则#4: 不得在 URI 中使用下划线(_)

文本查看器(如浏览器, 编辑器等)经常在 URI 下加下划线, 以提供可点击的视觉提示。根据应用程序的字体, 下划线(_)字符可能被这个下划线部分地遮蔽或完全隐藏。
为避免这种混淆, 请使用连字符( - )而不是下划线

规则#5: URI 路径中首选小写字母

方便的话, URI 路径中首选小写字母, 因为大写字母有时会导致问题。RFC 3986 中将 URI 定义为区分大小写, 但协议头和域名除外。例如:

http://api.example.com/my-folder/my-doc
HTTP://API.EXAMPLE.COM/my-folder/my-doc
# 在 URI 格式规范(RFC 3986)中这两个 URI 是相同的。

http://api.example.com/My-Folder/my-doc
# 而这个 URI 与上面的两个却是不同的。

规则#6: 文件扩展名不应包含在 URI 中

在 Web 上, 字符(.)通常用于分隔 URI 的文件名和扩展名。
一个 REST API 不应在 URI 中包含人造的文件扩展名, 来表示消息实体的格式。相反, 他们应该通过 header 头中 Content-Type 属性的媒体类型来确定如何处理实体的内容。

http://api.college.com/students/3248234/courses/2005/fall.json
http://api.college.com/students/3248234/courses/2005/fall

不应使用文件扩展名来表示格式偏好。
应鼓励 REST API 客户端使用 HTTP 提供的格式选择机制, 即请求 header 中的 Accept 属性。
为了实现简单的链接和调试的便捷, REST API 也可以通过查询参数来支持媒体类型的选择。

规则#7: 端点名称是单数还是复数?

这里采用保持简单的原则。虽然你的语法常识会告诉你使用复数来描述资源的单个实例是错误的, 但实际的答案是保持 URI 格式一致并且始终使用复数形式。
不必处理奇怪的复数(person/people, goose/geese), 这使 API 消费者的生活更美好, 也使 API 提供商更容易实现(因为大多数现代框架将在一个通用的 controller 中处理 /students 和 /students/3248234)。
但是你怎么处理关系呢?如果一个关系只能存在于另一个资源中, RESTful 原则可以提供有用的指导。我们来看一下这个例子。某个学生有一些课程。这些课程在逻辑上映射到端点 /students, 如下所示:

http://api.college.com/students/3248234/courses - 检索该学生所学习的所有课程清单, 学生编号为3248234。
http://api.college.com/students/3248234/courses/physics - 检索该学生的物理课程, 学生编号为3248234。

结论

当你设计 REST API 服务时, 你必须注意资源, 这些资源由 URI 定义。
你正在构建的服务中的每个资源, 都将至少有一个 URI 来标识它。这个 URI 最好是有意义的, 并能充分描述资源。URI 应遵循可预测的层次结构, 以增强可理解性, 从而提高可用性: 可预测的意义在于它们是一致的, 层次结构建立在数据具有结构关系的意义上。
RESTful API 是为消费者编写的。URI 的名称和结构应该向消费者传达意义。通过遵循上述规则, 你将创建一个更加清晰的 REST API。这不是一个 REST 规则或约束, 而是增强了 API。

五. 参考链接