HTTP Methods 和 RESTful Service API 设计

5,144 阅读10分钟
原文链接: www.jianshu.com

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 设计,具体探讨客户端该如何调用,服务端实现注意事项,以及数据安全和数据幂等性等注意事项。

  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/booksPOST https://api.server.com/books/123456/comments

  2. 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

  3. 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

  4. 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/123456PATCH https://api.server.com/books/123456/comments/id001注意这些 API URL 形式和 PUT API 没有区别,不同的只是 BODY 部分数据不同。

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

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

    API 举例DELETE https://api.server.com/books/123456DELETE 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