API 可以说是软件开发者的用户界面,API 设计也是系统架构的重要环节。尤其对复杂和分布式系统而言,其设计的好坏,直接影响着整个系统的设计,实现和演进。一套糟糕的 API 设计也会严重影响使用者(开发人员)的心情和工作效率。如果你对此表示怀疑并且打算进一步了解,可以先阅来自 Goolge 的一位大牛的分享: How to Design a Good API and Why it Matters[1]。
本系列的前一篇文章详细介绍了 REST 架构的理论和基础,而我们的最终目标是付诸实践和解决实际工程问题。本文将探讨 RESTful Service API 的一些基本设计方法和套路,包括常见的数据 CRUD API 设计,以及这些 API 应该如何返回信息等等。
约定和定义
在详细讨论 RESTful Service API 设计之前,我们先来解释和约定几个概念,以方便下文描述。在了解这些概念之前,假设你已经熟悉 HTTP 协议和 REST 架构。
-
HTTP Methods 也叫 HTTP Verbs,HTTP Methods 可以翻译成 HTTP 方法。它们是 HTTP 协议的一部分,主要规定了 HTTP 如何请求和操作服务器上的资源,常见的有GET,POST等。
-
APIApplication 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 所遵守的约定。它本身并不是一个规范和强制标准。遵守约定和套路的好处是可以避免产生方向性问题,也可以让使用的人感觉熟悉而容易上手,否则你可能需要额外的文来解释你的特殊设计。
HTTP Methods | 操作方式(CRUD) | 获取多个资源(/books)返回结果 | 获取单个资源(/books/id) 返回结果 |
---|---|---|---|
POST | 创建数据 Create | 201 (Created) HTTP Header 'Location' 值设置为/books/id,其中id为新创建的book id | 404 (Not Found), 如果资源已经存在,返回409 (Conflict) |
GET | 读取数据 Read | 200 (OK) 在Body中返回所有的books,可以使用参数来获取部分books数据如/books?page=3
|
200 (OK)在Body中返回对应id的book 404 (Not Found) 如果没有对应数据,或者id格式不对 |
PUT | 修改数据 Update 整条修改修改除ID外的所有属性 |
404 (Not Found) 除非该API要实现批量或者全部更新可返回200,否则一般直接返回404即可 | 200 (OK) 204 (No Content) 404 (Not Found), 如果id格式不正确或者没有找到 |
PATCH | 修改数据 Update 部分修改 修改一条记录的部分属性 |
404 (Not Found) 除非该API要实现批量或者全部更新可返回200,否则一般直接返回404即可 | 200 (OK) 204 (No Content) 404 (Not Found), 如果id格式不正确或者没有找到 |
DELETE | 删除数据 Delete | 404 (Not Found)一般直接返回404 除非你真的想删除全部集合可返回200 | 200 (OK)404 (Not Found) 如果id格式不正确或者没有找到 |
其中 HEAD,TRACE,OPTIONS,CONNECT 在 RESTful API 设计中不常用,这些 Methods 具体定义可以在这里找到。如果需要,可以根据相关语意来实现具有对应功能的API。
HTTP Methods 使用详细说明
上表列出了 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
-
GETGET 操作一般用于读取数据,即获取资源。成功调用 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
-
PUTPUT 一般用来更新记录,和 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
-
PATCHPATCH 操作只更新部分数据,比如有这么一条数据
{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 部分数据不同。 -
DELETEDELETE 应该很好理解,和其字面意义一样,用来删除一条数据。
正确删除后应该返回200,如果要删除的资源ID不存在返回404DELETE 在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 进行翻译和总结,敬请期待。
参考文档
[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 ↩