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时应该要考虑到的数据安全和数据幂等性等各方面注意事项。
-
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
-
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
-
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
-
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 部分数据不同。
-
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 设计最佳工程实践和常见问题解决方案)。
参考文档
-
原始链接在这里: http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/32713.pdf,须自备梯子 ↩
-
幂等性是指一次和多次请求某一个资源应该具有同样的副作用, 具体可参见https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html 或者http://www.cnblogs.com/weidagang2046/archive/2011/06/04/idempotence.html ↩