UP | HOME

RESTFUL API 设计最佳实践

Table of Contents

1 概述

Roy Fielding 于 2000 年在博士论文 《Architectural Styles and the Design of Network-based Software Architectures》(中文版) 提出 REpresentational State Transfer (REST) 概念,它倡导一种新的 web 架构风格,具有面向资源、松耦合、无状态、易扩展等特点,如今被广泛应用。

那么什么是 REST 风格呢,论文的第五章节 Representational State Transfer (REST) 做了详细说明,其中 uniform interface 是 REST 架构的最主要特征。

The central feature that distinguishes the REST architectural style from other network-based styles is its emphasis on a uniform interface between components…….

In order to obtain a uniform interface, multiple architectural constraints are needed to guide the behavior of components. REST is defined by four interface constraints: identification of resources; manipulation of resources through representations; self-descriptive messages; and, hypermedia as the engine of application state.

将 REST 架构风格与其他基于网络的风格区分开来的核心功能是强调组件之间的统一接口。

REST 架构有四个接口约束,其中前两个约束:identification of resources(资源识别) 和 manipulation of resources through representations(使用 representations 操作资源),它们表明 uniform interface(即 RESTful API) 操作的主体是 resource,即 REST 是面向 resource 的

那么什么是 resource 呢?任何能够被命名的事物都可称为 resource,比如某个文本、图像,甚至某种服务。在 web 中,我们采用 URI 来指代某个 resource。顾名思义,representation 就是资源的表现形式,比如图像资源的表现形式可为 JPEG image,文本资源的表现可为 text。

那么第三个约束 "self-descriptive messages(自我描述性的信息)" 表示什么呢?论文是这样解释的:即采用 standard methods(如 HTTP Method) 和 media types(如 HTTP Content type) 来表示操作类型,举例来说,我们可以用 GET 方法表示查询某个资源,用 DELETE 方法表示删除某个资源。

第四个约束 "hypermedia as the engine of application state(超媒体作为应用程序状态的引擎)" 表示 application 的状态由 request 决定,即客户端通过发送 request 来改变 application 的状态。以 HTTP POST 为例,客户端可以新建一个 resource,因而改变了 application 的状态。

总结而言,uniform interface 的约束条件定义了 web application API 的风格,即 RESTful API, 这种 API 是面向 resource 的,利用 (HTTP) 标准方法来描述操作,客户端通过 representation 来操作 resource,从而转化 resource 的状态

2 RESTful 最佳实践1

如何设计 RESTful API?

2.1 以资源为中心的 URL 设计

2.1.1 推荐用复数名词

推荐:

    /employees
    /employees/21

不推荐:

    /employee
    /employee/21

在资源集合 URL 上用 GET 方法,它更直观,特别是

    GET  /employees?state=external
    POST /employees
    PUT  /employees/56

2.1.2 每个资源使用两个 URL

资源集合用一个 URL,具体某个资源用一个 URL:

    /employees         #资源集合的 URL
    /employees/56      #具体某个资源的 URL

2.1.3 过滤搜索

提供对特定资源的搜索很容易。只需使用相应的资源集合 URL,并将搜索字符串附加到查询参数中即可。

不推荐做法:

    GET /internalEmployees
    GET /employees?state=internal

2.2 用 HTTP 方法操作资源(CURD)

使用 HTTP 方法来指定怎么处理这个资源。使用四种 HTTP 方法 POST,GET,PUT,DELETE 可以提供 CRUD 功能(创建,获取,更新,删除)。这让 API 更简洁,URL 数目更少。

不好的设计:

   /getAllEmployees
   /createEmployee
   /updateEmployee

更好的设计:

   GET /employees
   POST /employees
   PUT /employees/56

2 个 URL 乘以 4 个 HTTP 方法就是一组很好的功能。看看这个表格:

  POST(创建) GET(读取) PUT(更新) DELETE(删除)
/employees 创建一个新员工 列出所有员工 批量更新员工信息 删除所有员工
/employees/56 (错误) 获取 56 号员工的信息 更新 56 号员工的信息 删除 56 号员工

对资源集合的 URL 使用 POST 方法,创建新资源。

t015c68b6244b11b29e.png

Figure 1: 在资源集合 URL 上使用 POST 来创建新的资源过程

  1. 客户端向资源集合 URL/employees 发送 POST 请求。HTTP body 包含新资源的属性 “Albert Stark”。
  2. RESTful Web 服务器为新员工生成 ID,在其内部模型中创建员工,并向客户端发送响应。这个响应的 HTTP 头部包含一个 Location 字段,指示创建资源可访问的 URL。

对具体资源的 URL 使用 PUT 方法,来更新资源。

t01285dbf149a61a0fa.png

Figure 2: 使用 PUT 更新已有资源

  1. 客户端向具体资源的 URL 发送 PUT 请求/employee/21。请求的 HTTP body 中包含要更新的属性值(21 号员工的新名称 “Bruce Wayne”)。
  2. REST 服务器更新 ID 为 21 的员工名称,并使用 HTTP 状态码 200 表示更改成功。

2.3 非资源请求用动词

在实际资源操作中,总会有一些不符合 CRUD(Create-Read-Update-Delete) 的情况,一般有几种处理方法。

2.3.1 增加 endpoint

使用 POST 来执行动作,比如:

   GET /translate?from=de_DE&to=en_US&text=Hallo
   GET /calculate?para2=23&para2=432

在这种情况下,API 响应不会返回任何资源。而是执行一个操作并将结果返回给客户端。因此,应该在 URL 中使用动词而不是名词,来清楚的区分资源请求和非资源请求。

2.3.2 增加控制参数

添加资源动作相关的参数,通过修改参数来控制动作。比如一个博客网站,会有把写好的文章 “发布” 的功能,可以

POST /articles/id/publish

也可以在文章中增加 published 字段,发布的时候就是更新该字段

PUT /articles/id?published=true

2.3.3 把动作转换成资源

把动作转换成可以执行 CRUD 操作的资源,github 就是用了这种方法。

比如 “喜欢” 一个 gist,就增加一个 /gists/id/star 子资源,然后对其进行操作:“喜欢” 使用

PUT /gists/id/star

“取消喜欢” 使用

DELETE /gists/:id/star

另外一个例子是 Fork,这也是一个动作,但是在 gist 下面增加 forks 资源,就能把动作变成 CRUD 兼容的,可以执行用户 fork 的动作。

POST /gists/id/forks

2.4 使用标准 HTTP 响应码

RESTful Web 服务应使用合适的 HTTP 状态码来响应客户端请求。常用的响应码有:

请求成功:

  • 200 OK: 请求已成功,Body 有返回内容。多用作 GET Method 的 API 的返回码。
  • 201 Created: 请求已经被实现,资源被创建。多用作 POST Method 的同步类型 API 的返回码。
  • 202 Accepted: 服务器已接受请求,但尚未处理。多用作 POST Method 异步类型 API 的返回码。
  • 204 No Content: 服务器成功处理了请求,没有返回任何内容。用多于 DELETE/PUT Method 的 API 的返回码。

因客户端原因导致请求失败:

  • 400 Bad Request: 如参数错误,格式错误
  • 401 Unauthorized: 用户未被认证,如用密码错误,证书错误
  • 403 Forbidden: 用户权限不够
  • 404 Not Found: 服务端无此资源。通常为 URL 不存在,或者某个 Method 不存在
  • 409 Conflict: 请求存在冲突无法处理该请求

因服务端原因导致请求失败:

  • 500 Internal Server Error: 服务端错误消息,服务器遇到了一个未曾预料的状况。这是最常用的服务端错误代码
  • 501 Not Implemented: 服务器不支持当前请求所需要的某个功能
  • 503 Service Unavailable: 如服务器维护或者过载等

2.5 返回有用的错误提示

除了合适的状态码之外,还应该在 HTTP 响应正文中提供有用的错误提示和详细的描述。这是一个例子。 请求:

   GET /employees?state=super

响应:

   // 400 Bad Request
   {
    "message": "You submitted an invalid state. Valid state values are 'internal' or 'external'",
    "errorCode": 352,
    "additionalInformation" :
    "http://www.domain.com/rest/errorcode/352"
   }

2.6 在响应参数中添加浏览其它 API 的链接

理想情况下,不会让客户端自己构造使用 REST API 的 URL。让我们思考一个例子。 客户端想要访问员工的薪酬表。为此,他必须知道他可以通过在员工 URL(例如/employees/21/salaryStatements)中附加字符串 “salaryStatements” 来访问薪酬表。这个字符串连接很容易出错,且难以维护。如果更改了访问薪水表的 REST API 的方式(例如变成了/employees/21/salary-statement 或/employees/21/paySlips),所有客户端都将中断。 更好的方案是在响应参数中添加一个 links 字段,让客户端可以自动变更。 请求:

   GET /employees/

响应:

   {
      "id":1,
      "name":"Paul",
      "links": [
         {
            "rel": "salary",
            "href": "/employees/1/salaryStatements"
         }
      ]
   },

如果客户端完全依靠 links 中的字段获得薪资表,及时更改了 API,客户端将始终获得一个有效的 URL(只要更改了 link 字段,请求的 URL 会自动更改),不会中断。另一个好处是,API 变得可以自我描述,需要写的文档更少。

2.7 提供分页信息

一次性返回数据库所有资源不是一个好主意。因此,需要提供分页机制。通过两个参数来控制要返回的资源结果:

  • page:要获取哪一页的资源,默认是第一页
  • page_size:每页返回多少资源,如果没提供会使用预设的默认值;这个数量也是有一个最大值,不然用户把它设置成一个非常大的值(比如 99999999)也失去了设计的初衷。

这两个参数通常数据库中众所周知的 offset 和 limit。

   /employees?page=30&page_size=15       #返回 30 到 45 的员工

在分页时,还可以添加获取下一页或上一页的链接示例。只需提供适当的偏移和限制的链接示例。

   GET /employees?offset=20&limit=10
   {
   "offset": 20,
   "limit": 10,
   "total": 3465,
   "employees": [
    //...
   ],
   "links": [
     {
        "rel": "nextPage",
        "href": "/employees?offset=30&limit=10"
     },
     {
        "rel": "previousPage",
        "href": "/employees?offset=10&limit=10"
     }
   ]
   }

3 其他最佳实践

3.1 使用 HTTPS

这个和 RESTful API 本身没有很大的关系,但是对于增加网站的安全是非常重要的。特别如果提供的是公开 API,用户的信息泄露或者被攻击会严重影响网站的信誉。

NOTE:不要让非 SSL 的 url 访问重定向到 SSL 的 url。

3.2 验证和授权

一般来说,让任何人随意访问公开的 API 是不好的做法。验证和授权是两件事情:

验证(Authentication)是为了确定用户是其申明的身份,比如提供账户的密码。

授权(Authorization)是为了保证用户有对请求资源特定操作的权限。比如用户的私人信息只能自己能访问,其他人无法看到;有些特殊的操作只能管理员可以操作,其他用户有只读的权限等等。

3.3 HTTP Headers

3.3.1 Content-Type

标示 body 的数据格式。对于响应返回的格式,JSON 因为它的可读性、紧凑性以及多种语言支持等优点,成为了 HTTP API 最常用的返回格式。因此,最好采用 JSON 作为返回内容的格式。

如果用户需要其他格式,比如 xml,应该在请求头部 Accept 中指定。对于不支持的格式,服务端需要返回正确的 status code,并给出详细的说明。

3.3.2 Location

在响应 header 中使用,一般为客户端感兴趣的资源 URI, 例如在成功创建一个资源后,我们可以把新的资源 URI 放在 Location 中,如果是一个异步创建资源的请求,接口在响应 202 (“Accepted”) 的同时可以给予客户端一个异步状态查询的地址

3.4 API 地址

在 url 中指定 API 的版本是个很好地做法。如果 API 变化比较大,可以把 API 设计为子域名,比如 https://api.github.com/v3

3.5 API 版本

API 版本可以放在两个地方:

  • url,例如 https://example.com/api/v1
    • 优点:
      • 版本明确,方便调试。
      • 不同版本的协议解析可以放在不同的服务器上。
      • 不用考虑协议兼容性,开发方便,升级也不受影响。
    • 缺点:
      • 代码可能会有一定冗余。
  • HTTP Header 中
    • 优点:url 显得干净,符合 RESTful 惯例,毕竟版本号不属于资源的属性。
    • 缺点:需要根据解析头部,判断返回。

另外,不需要使用次级版本号(“v1.2”),因为不应该频繁的去发布 API 版本,只有当接口不兼容的时候才应该变更版本号。

3.6 分隔符

"/"分隔符一般用来对资源层级的划分,例如 http://api.canvas.restapi.org/shapes/polygons/quadrilaterals/squares

对于 REST API 来说,""只是一个分隔符,并无其他含义。为了避免混淆,""不应该出现在 URL 的末尾。例如以下两个地址实际表示的都是同一个资源:

http://api.canvas.restapi.org/shapes/
http://api.canvas.restapi.org/shapes

REST API 对 URI 资源的定义具有唯一性,一个资源对应一个唯一的地址。为了使接口保持清晰干净,如果访问到末尾包含 "" 的地址,服务端应该 301 到没有 ""的地址上。当然这个规则也仅限于 REST API 接口的访问,对于传统的 WEB 页面服务来说,并不一定适用这个规则。

3.7 连字符

URI 中尽量使用连字符 "-" 代替下划线 "_" 的使用。

连字符"-"一般用来分割 URI 中出现的字符串(单词),来提高 URI 的可读性,例如:

http://api.example.restapi.org/blogs/mark-masse/entries/this-is-my-first-post

使用下划线"_"来分割字符串(单词)可能会和链接的样式冲突重叠,而影响阅读性。

3.8 URI 中统一使用小写字母

根据 RFC3986 定义,URI 是对大小写敏感的,所以为了避免歧义,我们尽量用小写字符。但主机名(Host)和 scheme(协议名称:http/ftp/…)对大小写是不敏感的。

Footnotes:

Author: liushangliang

Email: phenix3443+github@gmail.com

Created: 2020-04-26 日 10:53