1. post 和 put 的区别
假如是客户端负责决定新资源采用什么URI,那就用PUT,假如是服务器负责新资源采用什么URI,那就用POST。
PUT请求来新建或修改资源 POST请求来新建从属资源。
表1-- PUT和POST动作
URI | 向新资源发PUT请求 | 向已有资源发PUT请求 | POST |
---|---|---|---|
/weblogs | N/A(资源已存在) | 无效果 | 创建一个新博客 |
/weblogs/myweblog | 创建该博客 | 修改该博客的设置 | 往博客里添加一篇文章 |
/weblogs/myweblog/entries/1 | N/A(你无法知道这个URI) | 编辑该博客的文章 | 为该博客文章添加评论 |
2. PUT和DELETE操作具有幂等性
幂等性:可以简单的理解为一次操作和多次操作的结果一样的
3. restful api设计原则
URL段 > 添加额外URL段 > POST返回 > 过滤器
要点提示
- 集合可以表达数据库表,资源可以表达数据库中的某条记录。事实上,API应该尽可能通过抽象来分离数据与业务逻辑。
- 至少知道4个半HTTP方法 GET/POST/PUT/PATCH/DELETE。
- API版本化,放在URL段里是不错的选择。
- 分析API的使用次数和将要抛弃的API的通知(有第三方的应用关注此条)。
- API根地址放到子域名,API基于HTTPS发布。
- 通过添加一个额外的URL段就可以实现更多的交互能力,定义关键字也是不错的主意。
- 能用端点(如GET /zoo/ZID/animals)表示的,不要用过滤器(如GET /animals?zoo_id=ZID)。
- 负载均衡配合状态码。
- 客户端创建资源时,它往往不知道新建资源ID(也许还有其他的属性,如创建和修改的时间戳等)。把属性作为POST请求的响应结果来返回。
- 超媒体API通过请求API的根来获得一个URL的列表,这个列表里面的每一个URL都指向一个集合,并且提供了客户端可以理解的信息来描述每一个集合。
- 创建一个控制台来让开发者可以立即体验一下API的功能。(时间充裕的情况下考虑)
术语
- 资源:一个对象的单独实例,如一只动物
- 集合:一群同种对象,如动物
- 客户端:可以创建HTTP请求的客户端应用程序
- 服务器:一个HTTP服务器或者应用程序,客户端可以跨网络访问它
- 端点:这个API在服务器上的URL用于表达一个资源或者一个集合
- 幂等:无边际效应,多次操作得到相同的结果
- URL段:在URL里面已斜杠分隔的内容
正文
- 数据设计与抽象
通常情况下,集合可以表达数据库表,资源可以表达数据库中的某条记录。事实上,你的API应该尽可能通过抽象来分离数据与业务逻辑。这点非常重要,只有这样做,才能满足复杂业务的第三方开发者,否则他们是不会想用你的API的。
当然,你的有些服务是不应该通过API暴露出去的,例如:很多API不允许第三方来创建用户的。
- http方法
HTTP方法你至少需要知道四个半。我之所以说“半个”的意思就是PATCH这个方法非常类似于PUT,并且它们俩也常常被开发者绑定到同一个API上。
- GET (获取):从服务器上获取一个具体的资源或者一个资源列表。
- POST (创建): 在服务器上创建一个新的资源。
- PUT (更新):以整体的方式更新服务器上的一个资源。
- PATCH (更新):只更新服务器上一个资源的一个属性。
- DELETE (删除):删除服务器上的一个资源。
还有两个不常用的HTTP方法:
- HEAD : 获取API方法的头部信息,如数据的哈希值或最后的更新时间。
- OPTIONS:获取客户端能对资源做什么操作的信息。
好的RESTful API只允许第三方调用者使用上述四个半的HTTP方法进行数据交互,并且在URL段里面不出现任何其他的方法关键字。
一般来说,GET请求可以被浏览器缓存(主要依赖请求头部),对于post的第二次提交而言也是如此。HEAD请求是基于GET的一个无响应体的请求,也是可以被缓存的。
- 版本化
即时你规划和准备的很充分,你核心的应用总会发生变化,数据关系也会变化,资源上的属性也会被增加或删除。只要你的项目还在,并且有大量用户在用,这种情况总是会有的。
请谨记一点,API是服务器与客户端之间的公共契约。如果你修改了服务器上的API,更改后无法向后兼容,那么你就打破了这个契约,客户端又会要求你重新支持它。为避免此类事件,你既要确保应用程序的迭代,又要让客户端满意。那么你必须在引入新版本API的同时,保持旧版本API仍然可用。
注:如果你只是简单的增加一个新特性到API上,如资源上的一个新属性或者增加一个新的端点,你不需要增加API的版本。因为这些并不会造成向后兼容性的问题,你只需要修改文档即可。
随着时间的推移,你可能声明不再支持某些旧版本的API。申明不支持一个特性并不意味着关闭或者破坏它。而是告诉客户端旧版本的API将在某个特定的时间被删除,并且建议他们使用新版本的API。
一个好的RESTful API会在URL中包含版本信息。常见的方案是在请求头里面携带版本信息。但是跟很多不同的第三方开发者一起工作后,我可以很明确的告诉你,在请求头里面包含版本信息远没有放在URL里面来的容易。
- 分析
API分析就是持续跟踪正在使用的API的版本和端点信息,这像每次请求都往数据库增加一个整数那样简单。有很多的迹象表明API跟踪分析优势所在,例如,对使用最广泛的API来说效率是最重要的。
第三方开发者通常会关注API构建目的,其中最重要的一个目的是你决定什么时候不再支持某个版本。但是你要告知开发者使用的那些即将被移除的API特性。当你准备删除旧的API之前可以通过这种方式提醒他们升级。
当然,对于第三方开发者的通知,我们可以限定某种条件下会被自动触发,例如每当在一个过时的特性上发生10000次请求时,就发邮件通知开发者。
- API根部URL
无论你信不信,API的根地址很重要。当一个开发者接手了一个旧项目(如进行代码review)。而这个项目正在使用你的API,同时开发者还想构建一个新的特性,但他们完全不知道你的服务。幸运的是他们知道客户端对外调用的那些URL列表。让你的API根入口点保持尽可能的简单是很重要的,因为开发者很可能一看到那些冗长而又复杂的URL就转身而走。
这里有两个常见的URL根例子:
https://example.org/api/v1/*
https://api.example.com/v1/*
如果应用很复杂,将API放到子域名下通常是一个不错的选择。这种做法可以保持某些规模化上的灵活性。
如果API不多,或是想让应用安装更简单(使用相同的框架来支持站点和API),将API放到根域名下也是ok的。
让API根拥有一些内容也是不错的,Github的API根就是一个典型的例子。
同样请注意HTTPS前缀,一个好的RESTful API总是基于HTTPS来发布的。
- 端点
一个端点就是指向特定资源或资源集合的URL。
如果你正在构建一个虚构的API来展现几个不同的动物园,每一个动物园又包含很多动物,员工和每个动物的物种,你可能会有如下的端点信息:
https://api.example.com/v1/zoos
https://api.example.com/v1/animals
https://api.example.com/v1/animal_types
https://api.example.com/v1/employees
针对每一个端点,你可能想列出所有可行的HTTP方法和端点的组合。如下所示,请注意我把HTTP方法都放在虚构API的前面,正如将同样的注解放在每一个HTTP请求头里一样。
GET /zoos: List all Zoos (ID and Name, not too much detail)
POST /zoos: Create a new Zoo
GET /zoos/ZID: Retrieve an entire Zoo object
PUT /zoos/ZID: Update a Zoo (entire object)
PATCH /zoos/ZID: Update a Zoo (partial object)
DELETE /zoos/ZID: Delete a Zoo
GET /zoos/ZID/animals: Retrieve a listing of Animals (ID and Name).
GET /animals: List all Animals (ID and Name).
POST /animals: Create a new Animal
GET /animals/AID: Retrieve an Animal object
PUT /animals/AID: Update an Animal (entire object)
PATCH /animals/AID: Update an Animal (partial object)
GET /animal_types: Retrieve a listing (ID and Name) of all Animal Types
GET /animal_types/ATID: Retrieve an entire Animal Type object
GET /employees: Retrieve an entire list of Employees
GET /employees/EID: Retreive a specific Employee
GET /zoos/ZID/employees: Retrieve a listing of Employees (ID and Name) who work at this Zoo
POST /employees: Create a new Employee
POST /zoos/ZID/employees: Hire an Employee at a specific Zoo
DELETE /zoos/ZID/employees/EID: Fire an Employee from a specific Zoo
在上面的列表里,ZID表示动物园的ID, AID表示动物的ID,EID表示雇员的ID,还有ATID表示物种的ID。
让文档API有一个关键字是个不错的主意。
为简洁起见,我已经省略了所有API共有的URL前缀。作为沟通方式这没什么问题,但是如果你真要写到API文档中,那就必须包含完整的路径(如,GET http://api.example.com/v1/animal_type/ATID)。
展示数据之间的关系,特别是雇员与动物园之间多对多关系。通过添加一个额外的URL段就可以实现更多的交互能力。当然没有HTTP方法能表示正在解雇一个人,但可以用DELETE一个动物园里的雇员来表示。
- 过滤器
当客户端创建一个请求来获取一个对象列表,必须要返回给一个符合查询条件的所有对象的列表。这个列表可能很大。但你不能随意给返回数据的数量做限制,因为限制会导致第三方开发者不知道发生了什么。如果他们请求一个确切的集合并且要遍历结果,然而他们发现只拿到了100条数据。接下来他们就不得不去查找这个限制条件的出处。到底是ORM的bug导致的,还是因为网络截断了大数据包?
尽可能减少会影响到第三方开发者的限制,这点很重要,但可以让客户端自己对结果做一些具体的过滤或限制。这么做最重要的一个原因是可以最小化网络传输,并让客户端尽快的得到查询结果。
其次是客户端可能比较懒,如果这时服务器能对结果做一些过滤或分页,对大家都是好事。另外一个不那么重要的原因是(从客户端角度来说),对服务器来说响应请求的负载越少越好。
过滤器是最有效的方式去处理那些获取资源集合的请求。所以只要出现GET的请求,就应该通过URL来过滤信息。以下有一些过滤器的例子,可能是你想要填加到API中的:
?limit=10: 减少返回给客户端的结果数量(用于分页)
?offset=10: 发送一堆信息给客户端(用于分页)
?animal_type_id=1: 使用条件匹配来过滤记录
?sortby=name&order=asc: 对结果按特定属性进行排序
有些过滤器可能会与端点URL的效果重复。例如我之前提到的GET /zoo/ZID/animals。
它也同样可以通过GET /animals?zoo_id=ZID来实现。
独立的端点会让客户端更好过一些,因为他们的需求往往超出你的预期。本文中提到这种冗余差异可能对第三方开发者并不可见。
无论怎么样,当准备过滤或排序数据时,必须明确的将那些可以过滤或排序的数据字段放到白名单中,因为我们不想将任何的数据库错误发送给客户端。
- 状态码
RESTful API使用HTTP的状态码至关重要,因为它们是HTTP的标准。很多的网络设备都可以识别这些状态码,例如某台服务器已经发送了很多50x错误回来,负载均衡器可能会通过配置来避免继续发送请求到这台服务器上。
- 预期的返回文档
使用不同的HTTP方法向服务器请求时,客户端需要在返回结果里拿到一系列的信息。下面的列表是非常经典的RESTful API定义:
GET /collection: 返回一系列资源对象
GET /collection/resource: 返回单独的资源对象
POST /collection: 返回新创建的资源对象
PUT /collection/resource: 返回完整的资源对象
PATCH /collection/resource: 返回完整的资源对象
DELETE /collection/resource: 返回一个空文档
当一个客户端创建一个资源时,它往往不知道新建资源ID(也许还有其他的属性,如创建和修改的时间戳等)。这些属性将在随后的请求中返回,并且作为刚才POST请求的一个响应结果。
- 认证
服务器在大多数情况下是想确切的知道谁创建了什么请求。当然,有些API是提供给公共用户(匿名用户)的,但是大部分时间里也是代表某人个用户。
OAuth2.0提供了一个非常好的方式去处理这种问题。在每个请求里,可以明确知道哪个客户端创建了请求,哪个用户提交了请求,并且提供了一种标准的访问过期机制或允许用户从客户端注销,所有这些都不需要第三方的客户端知道用户的登陆认证信息。
还有OAuth1.0和xAuth同样适用这种场景。无论选择哪个方法,请确保它为多种不同语言/平台上的库提供了一些通用的并且设计良好文档,因为你的用户可能会使用这些语言和平台来编写客户端。
- Content Type(内容类型)
目前,大多数“精彩”的API都为RESTful接口提供JSON数据。诸如Facebook,Twitter,Github等等你所知的。XML曾经也火过一把(通常在一个大企业级环境下)。这要感谢SOAP,不过它已经挂了,并且我们也没看到太多的API把HTML作为结果返回给客户端(除非你在构建一个爬虫程序)。
只要你返回给他们有效的数据格式,开发者就可以使用流行的语言和框架进行解析。如果你正在构建一个通用的响应对象,通过使用一个不同的序列化器,你也可以很容易的提供之前所提到的那些数据格式(不包括SOAP)。而你所要做的就是把使用方式放在响应数据的接收头里面。
有些API的创建者会推荐把.json, .xml, .html等文件的扩展名放在URL里面来指示返回内容类型,但我个人并不习惯这么做。我依然喜欢通过接收头来指示返回内容类型(这也是HTTP标准的一部分),并且我觉得这么做也比较友好。
- 超媒体API
超媒体API很可能就是RESTful API设计的将来。超媒体是一个非常棒的概念,它回归到了HTTP和HTML如何运作的“本质”。
在非超媒体RESTful API的情景中,URL端点是服务器与客户端契约的一部分。这些端点必须让客户端事先知道,并且修改它们也意味着客户端可能再也无法与服务器通信了。你可以先假定这是一个限制。
时至今日,英特网上的API客户端已不仅仅只有那些创建HTTP请求的用户代理了。大多数HTTP请求是由人们通过浏览器产生的。人们不会被哪些预先定义好的RESTful API端点URL所束缚。是什么让人们变的如此与众不同?因为人们可以阅读内容,可以点击他们感兴趣的链接,并浏览一下网站,然后跳到他们关注的内容那里。即使一个URL改变了,人们也不会受到影响(除非他们事先给某个页面做了书签,这时他们回到主页并发现原来有一条新的路径可以去往之前的页面)。
超媒体API概念的运作跟人们的行为类似。通过请求API的根来获得一个URL的列表,这个列表里面的每一个URL都指向一个集合,并且提供了客户端可以理解的信息来描述每一个集合。是否为每一个资源提供ID并不重要(或者不是必须的),只要提供URL即可。
一个超媒体API一旦具有了客户端,那么它就可以爬行链接并收集信息,而URL总是在响应中被更新,并且不需要如契约的一部分那样事先被知晓。如果一个URL曾经被缓存过,并且在随后的请求中返回404错误,那么客户端可以很简单的回退到根URL并重新发现内容。
在获取集合中的一个资源列表时会返回一个属性,这个属性包含了各个资源的完整URL。当实施一个POST/PATCH/PUT请求后,响应可以被一个3xx的状态码重定向到完整的资源上。
JSON不仅告诉了我们需要定义哪些属性作为URL,也告诉了我们如何将URL与当前文档关联的语义。正如你猜的那样,HTML就提供了这样的信息。我们很欣慰看到我们的API走过了完整的周期,并回到了处理HTML上来。想一下我们与CSS一起前行了多远,有一天我们可能再次看到它变成了一个通用实践让API和网站可以去使用相同的URL和内容。
- 文档
老实说,即使你不能完全遵循指南中的条款,你的API也不会那么糟糕。但是,如果你不为API准备文档的话,没有人会知道怎么使用它,那它真的会成为一个糟糕的API。
让你的文档对那些未经认证的开发者也可用,不要使用文档自动化生成器,即便你用了,你也要保证自己审阅过并让它具有更好的版式。
不要截断示例中请求与响应的内容,要展示完整的东西。并在文档中使用高亮语法。
文档化每一个端点所预期的响应代码和可能的错误消息,和在什么情况下会产生这些的错误消息。
如果时间充足,那就创建一个控制台来让开发者可以立即体验一下API的功能。创建一个控制台并没有想象中那么难,并且开发者们(内部或者第三方)也会因此而欣赏你。
另外确保你的文档能够被打印。CSS是个强大的工具可以帮助到你。而且在打印的时候也不用太担心边侧栏的问题。即便没有人会打印到纸上,你也会惊奇的发现很多开发者愿意转化成PDF格式进行离线阅读。
- 勘误:原始的HTTP封包
因为上述都是基于HTTP协议,所以我将展示给你一个解析后的HTTP包。我很惊讶的发现,大部分人不知道这些东西。当客户端发送一个请求到服务器时,他们会提供一个键值对集,先是一个头,紧跟着是两个回车换行符,然后才是请求体。所有这些都是在一个封包里被发送。
服务器响应也是同样的键值对集,带两个回车换行符,然后是响应体。HTTP就是一个请求/响应协议;它不支持“推送”模式(服务器直接发送数据给客户端),除非你采用其他协议,如Websocket。
当你设计API时,你应该能够使用工具去查看原始的HTTP包。Wireshark是个不错的选择。同时,你也该采用一个框架/web服务器,使你能够在必要时修改某些字段的值。
Example HTTP Request
POST /v1/animal HTTP/1.1
Host: api.example.org
Accept: application/json
Content-Type: application/json
Content-Length: 24
{
"name": "Gir",
"animal_type": 12
}
Example HTTP Response
HTTP/1.1 200 OK
Date: Wed, 18 Dec 2013 06:08:22 GMT
Content-Type: application/json
Access-Control-Max-Age: 1728000
Cache-Control: no-cache
{
"id": 12,
"created": 1386363036,
"modified": 1386363036,
"name": "Gir",
"animal_type": 12
}