HTTP Methods 和 RESTful Service API 设计

96
JeffreyLi
2016.08.22 08:05* 字数 3384

API 可以说是软件开发者的用户界面,API 设计也是系统架构的重要环节。尤其对复杂和分布式系统而言,其设计的好坏,直接影响着整个系统的设计,实现和演进。一套糟糕的 API 设计也会严重影响使用者(开发人员)的心情和工作效率。如果你对此表示怀疑并且打算进一步了解,可以先了解下来自 Goolge 的一位大牛的分享: How to Design a Good API and Why it Matters[1]

本系列的前一篇文章详细介绍了 REST 架构的理论和基础,而我们的最终目标是付诸实践和解决实际工程问题。本文将探讨 RESTful Service API 的一些基本设计方法和套路,主要包含常见数据 CRUD 设计和这些 API 的输入和输出格式等等。

约定和定义

在详细讨论 RESTful Service API 设计之前,我们先来解释和约定几个概念,以方便下文描述。在了解这些概念之前,假设你已经熟悉 HTTP 协议REST 架构

  • HTTP Methods
    也叫 HTTP Verbs,HTTP Methods 可以翻译成 HTTP 方法。它们是 HTTP 协议的一部分,主要规定了 HTTP 如何请求和操作服务器上的资源,常见的有GET,POST等。

  • API
    Application Programming Interface 应用程序接口。如果没有特别的说明,本文中提到的 API 均指 RESTful Web Service API 简称 RESTful API。这类API是通过 HTTP 协议 URL 形式暴露给其它系统或者模块调用,比如,一个获得用户所有评论的 API 可能像这样:
    https://api.server-name.com/user-id/comments

使用 HTTP Methods 构建 RESTful API

HTTP Methods 一共有九个,分别是 GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,CONNECT,PATCH。在RESTful API 设计中,常用的有POST,GET,PUT,PATCH 和 DELETE,分别对应对资源的创建,获取,修改,部分修改和删除操作。下表简单列出了这些Methods的用途和返回值约定。

HTTP Methods 用途一览

这是一个推荐的 Best Practise 和大多数现有 API 所遵守的约定。它本身并不是一个规范和强制标准。遵守约定和套路的好处是可以避免原则性设计缺陷,也可以让经常使用此类 API 的开发者感觉熟悉和容易上手,否则你可能需要额外的文档来解释你的特殊设计,增加了使用者的负担和学习成本。

| HTTP Methods | 操作方式(CRUD) | 获取多个资源(/books)返回结果 |获取单个资源(/books/id) 返回结果
|:---------|:--------------|:--|
| POST| 创建数据 Create| 201 (Created) <br /> HTTP Header 'Location' 值设置为/books/id,其中id为新创建的book id | 404 (Not Found), <br />如果资源已经存在,返回409 (Conflict)|
| GET| 读取数据 Read |200 (OK) <br /> 在Body中返回所有的books,可以使用参数来获取部分books数据如/books?page=3 | 200 (OK)<br />在Body中返回对应id的book<br /><br />404 (Not Found) <br />如果没有对应数据,或者id格式不对 |
| PUT| 修改数据 Update <br /><br />整条修改<br />修改除ID外的所有属性 |404 (Not Found) <br />除非该API要实现批量或者全部更新可返回200,否则一般直接返回404即可 |200 (OK) <br /> 204 (No Content)<br /> 404 (Not Found), 如果id格式不正确或者没有找到|
| PATCH| 修改数据 Update <br /><br />部分修改 <br /> 修改一条记录的部分属性 |404 (Not Found) <br />除非该API要实现批量或者全部更新可返回200,否则一般直接返回404即可 |200 (OK) <br /> 204 (No Content)<br /> 404 (Not Found), 如果id格式不正确或者没有找到 |
| DELETE| 删除数据 Delete| 404 (Not Found)<br />一般直接返回404 除非你真的想删除全部集合可返回200 |200 (OK)
404 (Not Found) 如果id格式不正确或者没有找到 |

其中 HEAD,TRACE,OPTIONS,CONNECT 在 RESTful API 设计中不常用,这些 Methods 具体定义可以在这里找到。如果需要,可以根据相关语意来实现具有对应功能的API。

HTTP Methods 使用详细说明

对照上面的表格,我们来一一探讨每种 API 在客户端应该如何调用,在服务端如何实现,以及在设计这些API时应该要考虑到的数据安全和数据幂等性等各方面注意事项。

  1. POST
    使用 POST 的 API 一般用来表示创建一条数据。举例来说,如果要设计一个向后端数据库添加一条关于图书信息等 API,可以设计成:
    https://api.server.com/books

客户端调用
客户端把要创建的数据放在HTTP请求的Body中,比如Body数据是
{title: "Are your lights on", author: "Donald C. Gause"}
之后发送 HTTP POST 请求到https://api.server.com/books

服务端实现
a) 服务端在收到客户端 POST 来的数据时,根据POST URL,发现应该创建books数据。
b) 之后获取 body 里面的内容来创建一条新 book 记录并保存,如果一切正常,返回201表示创建成功。
c) 返回时将 HTTP Header 'Location' 值设置为
https://api.server.com/books/new-created-book-id
之后客户端可以获得该条刚创建数据的 Unique ID,方便在需要进一步操作时使用(为什么需要返回这个 Unique ID 可以参见RESTful Web Service 架构剖析 约定6.2 Resource Identifiers)

值得注意的是 POST API 不是一个数据安全和幂等性[2]操作,如果客户端多次调用同样的 API 会导致多条数据被创建,这些数据除了 ID 不同其他属性都相同。

API举例
POST https://api.server.com/books
POST https://api.server.com/books/123456/comments

  1. GET
    GET 操作一般用于读取数据,即获取资源。成功调用 GET API 会返回相应的数据。如果请求的数据不存在可返回404(Not Found)或者由于参数不正确的原因可以返回400(Bad Request)

客户端调用
客户端只要简单发送一个 HTTP GET 请求到相应的 URL 即可,请求URL 中可以带上有关参数用来对数据进行条件过滤,如:
GET https://api.server.com/books?author=gause

服务端实现
服务端在收到相应的请求之后根据 URL 判断应该返回什么类型的数据,并且根据 URL 参数对数据进行过滤后在放在 Body 中返回给客户端。GET 可以返回一个集合,类似数组的形式。比如返回的数据可能是这样的:

{
    result: "true"
    data: [
        { title: "Are your lights on", author: "Donald C. Gause" },
        { title: "another", author: "anthor a" },
        { title: "book title", author: "anthor b" },
    ]
}

如果客户端只请求一条数据 GET https://api.server.com/books/000
应该返回对应ID的数据即可:

{
    result: "true"
    data: { id: "000", title: "another", author: "anthor a" }
}

注意,这里返回的数据格式仅用于举例,实际格式可以根据不同的需求可能差别很大。

GET操作是数据安全和具有幂等性的操作,也就是多次调用GET应该返回相同的数据(期间没有修改操作的前提下),并且不会导致任何数据的破坏性修改。

API举例
GET https://api.server.com/books/123456
GET https://api.server.com/books/123456/comments
GET https://api.server.com/books/123456/comments/id001
GET https://api.server.com/books?author=gause

  1. PUT
    PUT 一般用来更新记录,和 PATCH 不同的是,PUT 一般用于替换该记录的所有属性。PATCH 只是部分更新。和 POST 不同的是,PUT 不会生成新的资源 ID,而 POST 会生成并且返回新创建的数据 ID

客户端调用
和 POST 调用方式几乎相同,比如要修改的数据是
{id: "book-id-000", title: "Are your lights on", author: "Donald C. Gause"}
客户端发送 HTTP PUT 请求到https://api.server.com/books。和POST不同的是,该操作会带上数据的 UID,用来定位具体要修改的这一条数据,方便后续操作。
也有的设计会把ID放在URL中https://api.server.com/books/book-id-000,这样要修改的Body中的数据可以不用包含ID

服务端实现
如果更新成功 PUT API 应该返回200。如果 PUT 请求的 body 中没有任何信息则返回204, 如果id没有找到或id格式不正确,返回404。和POST不同的是该 API 没有必要在Header中更新刚创建数据ID URL,因为我们是在修改该条数据,其 ID 之前已经被客户的获取。

PUT 操作不是数据安全的,因为这个操作改变了数据,但是PUT操作是幂等性的,对于相同的PUT请求,无论调用多少次,造成的数据修改的结果永远和调第一次时相同。

API举例
PUT https://api.server.com/books/123456
PUT https://api.server.com/books/123456/comments/id001

  1. PATCH
    PATCH 操作只更新部分数据,比如有这么一条数据
    {id:000, title: "Are your lights on", author: "Donald C. Gause", pub:"xyz"}
    PATCH 操作可能只是修改 title,或者修改 pub,具体修改的内容由body 里面的数据格式规定。而 PUT API 中 body 数据一般是要替换所有数据的属性(除了ID以外)。

客户端调用
和 PUT 不同的只是 Body 的数据格式,PUT 请求的 Body 一般是这样的
{title: "Are your lights on"} 只包含部分要修改的数据。

服务端实现
服务端根据 Body 的内容对该条数据进行部分更新。成功更新数据应该返回200,当数据 ID 没有找到返回404。

注意 PATCH 操作其实不是幂等性操作,也不是数据安全的,来自不同的客户端的 PATCH 请求可能让数据部分属性相互覆盖和冲突。PATCH的幂等性可能不是很好理解,举例来说明:
假设第一个PATCH 请求A 操作导致 book 数据修改成
{id:000, title: "AAA", author: "AAA", pub:"xyz"}
这时候如果其他客户端一个发出一个 PATCH 请求B 将数据改成
{id:000, title: "AAA", author: "AAA", pub:"BBB"}
如果此时重复调用 请求A,虽然要修改的数据部分属性是相同的,但对于整条数据本身而言,已经和第一次调用结束时不完全相同了。不像PUT操作,整个修改之后不会造成数据有部分不相同的情况,PUT请求即使在多次相同的调用期间,其他客户端修改了数据,最后一次调用之后和第一次调用之后数据依然相同的。

API举例
PATCH https://api.server.com/books/123456
PATCH https://api.server.com/books/123456/comments/id001
注意这些 API URL 形式和 PUT API 没有区别,不同的只是 BODY 部分数据不同。

  1. DELETE
    DELETE 应该很好理解,和其字面意义一样,用来删除一条数据。

正确删除后应该返回200,如果要删除的资源ID不存在则返回404
DELETE 在HTTP 协议语义中是幂等性的,无论调用多少次之后,该数据都是同样的被删除状态。
虽然第一次调用的结果(200)和之后的调用的结果(数据已经不存在会返回404)有所不同,但数据本身没有变化,对数据而言,它依然具有幂等性。
不过,如果服务端维护了一些统计数据,就会破坏幂等性,因为DELETE导致了统计数据的减少。

** API 举例**
DELETE https://api.server.com/books/123456
DELETE https://api.server.com/books/123456/comments/id001

结束语

本来打算在本篇文章中讨论一些关于“常用问题解决方案和最佳工程实践(Best Practices)”这类更加贴近实际操作层面的内容,不过写着写着发现本篇内容已经足够多了。为了降低读者理解负担和限于篇幅,打算另写一篇文章。下一篇主要是对这篇文章 Best Practices for Designing a Pragmatic RESTful API 进行翻译和总结,敬请期待(已更新:RESTful Service API 设计最佳工程实践和常见问题解决方案)。

参考文档


  1. 原始链接在这里: http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/32713.pdf,须自备梯子

  2. 幂等性是指一次和多次请求某一个资源应该具有同样的副作用, 具体可参见https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html 或者http://www.cnblogs.com/weidagang2046/archive/2011/06/04/idempotence.html

Web note ad 1