Web API设计实际上是一个挺重要的设计话题,许多公司都会有公司层面的Web API设计规范,几乎全部的项目在详细设计阶段都会进行API设计,项目开发后都会有一份API文档供测试和联调。本文尝试根据本身的理解总结一下目前常见的四种API设计风格以及设计考虑点。html
RPC
这是最多见的方式,RPC说的是本地调用远程的方法,面向的是过程。前端
- RPC形式的API组织形态是类和方法,或者说领域和行为。
- 所以API的命名每每是一个动词,好比GetUserInfo,CreateUser。
- 由于URI会很是多并且每每没有一些约定规范,因此须要有详细的文档。
- 也是由于无拘无束,HTTP方法基本只用GET和POST,设计起来比较简单。
这里就不贴例子了,估计超过50%的API是这种分格的。java
REST
是一种架构风格,有四个级别的成熟度:node
- 级别 0:定义一个 URI,全部操做是对此 URI 发出的 POST 请求。
- 级别 1:为各个资源单首创建 URI。
- 级别 2:使用 HTTP 方法来定义对资源执行的操做。
- 级别 3:使用超媒体(HATEOAS)。
级别0其实就是类RPC的风格,级别3是真正的REST,大多数号称REST的API在级别2。REST实现一些要点包括:git
- REST形式的API组织形态是资源和实体,一切围绕资源(级别1的要点)。设计流程包括:
- 肯定API提供的资源
- 肯定资源之间的关系
- 根据资源类型和关系肯定资源URI结构
- 肯定资源的结构体
- 会定义一些标准方法(级别2的要点),而后把标准方法映射到实现(好比HTTP Method):
- GET:获取资源详情或资源列表。对于collection类型的URI(好比/customers)就是获取资源列表,对于item类型的URI(好比/customers/1)就是获取一个资源。
- POST:建立资源,请求体是新资源的内容。每每POST是用于为集合新增资源。
- PUT:建立或修改资源,请求体是新资源的内容。每每PUT用于单个资源的新增或修改。实现上必须幂等。
- PATCH:部分修改资源,请求体是修改的那部份内容。PUT通常要求提交整个资源进行修改,而PATCH用于修改部份内容(好比某个属性)。
- DELETE:移除资源。和GET同样,对于collection类型的URI(好比/customers)就是删除全部资源,对于item类型的URI(好比/customers/1)就是删除一个资源。
- 须要考虑资源之间的导航(级别3的要点,好比使用HATEOAS,HATEOAS是Hypertext as the Engine of Application State的缩写)。有了资源导航,客户端甚至可能不须要参阅文档就能够找到更多对本身有用的资源,不过HATEOAS没有固定的标准,好比:
{
"content": [ { "price": 499.00, "description": "Apple tablet device", "name": "iPad", "links": [ { "rel": "self", "href": "http://localhost:8080/product/1" } ], "attributes": { "connector": "socket" } }, { "price": 49.00, "description": "Dock for iPhone/iPad", "name": "Dock", "links": [ { "rel": "self", "href": "http://localhost:8080/product/3" } ], "attributes": { "connector": "plug" } } ], "links": [ { "rel": "product.search", "href": "http://localhost:8080/product/search" } ] }
Spring框架也提供了相应的支持:https://spring.io/projects/spring-hateoas,好比以下的代码:github
@RestController public class GreetingController { private static final String TEMPLATE = "Hello, %s!"; @RequestMapping("/greeting") public HttpEntity<Greeting> greeting( @RequestParam(value = "name", required = false, defaultValue = "World") String name) { Greeting greeting = new Greeting(String.format(TEMPLATE, name)); greeting.add(linkTo(methodOn(GreetingController.class).greeting(name)).withSelfRel()); return new ResponseEntity<>(greeting, HttpStatus.OK); } }
产生以下的结果:spring
- 除了以前提到的几个要点,REST API的设计还有一些小点:
- 必须无状态的,相互独立的,不区分顺序的
- API须要有一致的接口来解耦客户端和服务实现,若是基于HTTP那么务必使用HTTP的Method来操做资源,并且尽可能使用HTTP响应码来处理错误
- 须要尽可能考虑缓存、版本控制、内容协商、部分响应等实现
能够说REST的API设计是须要设计感的,须要仔细来思考API的资源,资源之间的关系和导航,URI的定义等等。对于一套设计精良的REST API,其实客户端只要知道可用资源清单,每每就能够轻易根据约定俗成的规范以及导航探索出大部分API。比较讽刺的是,有不少网站给前端和客户端的接口是REST的,爬虫开发者能够轻易探索到全部接口,甚至一些内部接口,毕竟猜一下REST的接口比RPC的接口容易的多。json
做为补充,下面再列几个有关REST API设计你们争议讨论纠结的比较多的几个方面。api
建立资源使用PUT仍是POST
好比 https://stackoverflow.com/questions/630453/put-vs-post-in-rest ,总的来讲你们基本认同微软提到的三个方面:缓存
- 客户端决定资源名用PUT,服务端决定资源名用POST
- POST是把资源加入集合
- PUT实现须要幂等
固然,有些公司的规范是建立资源仅仅是POST,不支持PUT
异常处理的HTTP响应状态码
- REST的建议是应当考虑尽量使用匹配的Http状态码来对应到错误类型,好比删除用户的操做:
- 用户找不到是404
- 删除成功后是204
- 用户由于有帐户余额没法删除是409(客户端的问题是4xx)
- 其它服务端异常是500(服务端的问题是5xx)
- 整体来讲这个规范出发点是好的,实现起来落地比较困难,缘由有下面几个:
- 状态码对应各类错误类型的映射关系没有统一标准,工程师实现的时候五花八门
- 实现起来可能须要在业务逻辑中耦合状态码,很难在GlobalExceptionHandler去作,除非事先先规范出十几种异常
- 若是使用了不正确的响应状态可能会致使反向代理等触发错误的一些操做,并且出现问题的时候搞不清楚是哪一个层面出错了
- 各类Http Client对应非200状态码的处理方式不太一致
- 有关这个问题的争议,各大平台的API实现有些听从这个规范建议,有些是500甚至200打天下的,相关的国内外讨论有:
- https://stackoverflow.com/questions/27921537/returning-http-200-ok-with-error-within-response-body
- https://www.zhihu.com/question/268409269/
- https://www.zhihu.com/question/58686782
- https://blog.cloud-elements.com/error-handling-restful-api-design-part-iii
- 国内外的不少大厂对于这点的实现不尽相同,总的来讲,个人建议是:
- 若是咱们明确API是REST的,并且API对外使用,应当使用合适的状态码来反映错误(建议控制在20个之内经常使用的),而且在文档中进行说明,并且出错后须要在响应体补充细化的error信息(包含code和message)
- 若是REST API对内使用,那么在客户端和服务端商量好统一标准的状况下能够对响应码类型进行收敛到几个,实现起来也方便
- 若是API是内部使用的RPC over HTTP形式,甚至能够退化到业务异常也使用200响应返回
返回数据是否须要包装
看到过许多文章都在说,REST仍是建议返回的数据自己就是实体信息(或列表信息),而不建议把数据进行一层包装(Result)。若是须要有更多的信息来补充的话,能够放到HTTP Header中,好比https://developer.github.com/v3/projects/cards/的API:
GET /projects/columns/:column_id/cards Status: 200 OK Link: <https://api.github.com/resource?page=2>; rel="next", <https://api.github.com/resource?page=5>; rel="last" [ { "url": "https://api.github.com/projects/columns/cards/1478", "id": 1478, "node_id": "MDExOlByb2plY3RDYXJkMTQ3OA==", "note": "Add payload for delete Project column", "created_at": "2016-09-05T14:21:06Z", "updated_at": "2016-09-05T14:20:22Z", "archived": false, "column_url": "https://api.github.com/projects/columns/367", "content_url": "https://api.github.com/repos/api-playground/projects-test/issues/3", "project_url": "https://api.github.com/projects/120" } ]
以前咱们给出的HATEOAS的例子是在响应体中有"content"和"links"的层级,也就是响应体并非资源自己,是有包装的,除了links,不少时候咱们会直接以统一的格式来定义API响应结构体,好比:
{
"code" : "", "message" : "", "path" : "" "time" : "", "data" : {}, "links": [] }
我我的比较喜欢这种方式,不喜欢使用HTTP头,缘由仍是由于多变的部署和网络环境下,若是某些环节请求头被修改了或丢弃了会很麻烦(还有麻烦的Header Key大小写问题),响应体通常全部的代理都不会去动。
URI的设计层级是否超过两层
微软的API设计指南(文末有贴地址)中指出避免太复杂的层级资源,好比/customers/1/orders/99/products过于复杂,能够退化为/customers/1/orders和/orders/99/products,不URI的复杂度不该该超过collection/item/collection,Google的一些API会层级比较多,好比:
API service: spanner.googleapis.com A collection of instances: projects/*/instances/*. A collection of instance operations: projects/*/instances/*/operations/*. A collection of databases: projects/*/instances/*/databases/*. A collection of database operations: projects/*/instances/*/databases/*/operations/*. A collection of database sessions: projects/*/instances/*/databases/*/sessions/*
这点我比较赞同微软的规范,太深的层级在实现起来也不方便。
GraphQL
若是说RPC面向过程,REST面向资源,那么GraphQL就是面向数据查询了。“GraphQL 既是一种用于 API 的查询语言也是一个知足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端可以准确地得到它须要的数据,并且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。”
采用GraphQL,甚至不须要有任何的接口文档,在定义了Schema以后,服务端实现Schema,客户端能够查看Schema,而后构建出本身须要的查询请求来得到本身须要的数据。
好比定义以下的Schema:
# # Schemas must have at least a query root type # schema { query: Query } type Query { characters( episode: Episode ) : [Character] human( # The id of the human you are interested in id : ID! ) : Human droid( # The non null id of the droid you are interested in id: ID! ): Droid } # One of the films in the Star Wars Trilogy enum Episode { # Released in 1977 NEWHOPE # Released in 1980. EMPIRE # Released in 1983. JEDI } # A character in the Star Wars Trilogy interface Character { # The id of the character. id: ID! # The name of the character. name: String! # The friends of the character, or an empty list if they # have none. friends: [Character] # Which movies they appear in. appearsIn: [Episode]! # All secrets about their past. secretBackstory : String @deprecated(reason : "We have decided that this is not canon") } # A humanoid creature in the Star Wars universe. type Human implements Character { # The id of the human. id: ID! # The name of the human. name: String! # The friends of the human, or an empty list if they have none. friends: [Character] # Which movies they appear in. appearsIn: [Episode]! # The home planet of the human, or null if unknown. homePlanet: String # Where are they from and how they came to be who they are. secretBackstory : String @deprecated(reason : "We have decided that this is not canon") } # A mechanical creature in the Star Wars universe. type Droid implements Character { # The id of the droid. id: ID! # The name of the droid. name: String! # The friends of the droid, or an empty list if they have none. friends: [Character] # Which movies they appear in. appearsIn: [Episode]! # The primary function of the droid. primaryFunction: String # Construction date and the name of the designer. secretBackstory : String @deprecated(reason : "We have decided that this is not canon") }
采用GraphQL Playground(https://github.com/prisma/graphql-playground)来查看graphql端点能够看到全部支持的查询:
其实就是__schema:
而后咱们能够根据客户端的UI须要本身来定义查询请求,服务端会根据客户端给的结构来返回数据:
再来看看Github提供的GraphQL(更多参考https://developer.github.com/v4/guides/):
查询出了最后的三个个人repo:
GraphQL就是经过Schema来明确数据的能力,服务端提供统一的惟一的API入口,而后客户端来告诉服务端我要的具体数据结构(基本能够说不须要有API文档),有点客户端驱动服务端的意思。虽然客户端灵活了,可是GraphQL服务端的实现比较复杂和痛苦的,GraphQL不能替代其它几种设计风格,并非传说中的REST 2.0。更多信息参见 https://github.com/chentsulin/awesome-graphql 。
服务端驱动API
没有高大上的英文缩写,由于这种模式或风格是我本身想出来的,那就是经过API让服务端来驱动客户端,在以前的一些项目中也有过实践。说白了,就是在API的返回结果中包含驱动客户端去怎么作的信息,两个层次:
- 交互驱动:好比包含actionType和actionInfo,actionType能够是toast、alert、redirectView、redirectWebView等,actionInfo就是toast的信息、alert的信息、redirect的URL等。由服务端来明确客户端在请求API后的交互行为的好处是:
- 灵活:在紧急的时候还能够经过redirect方式进行救急,好比遇到特殊状况须要紧急进行逻辑修改能够直接在不发版的状况下切换到H5实现,甚至咱们能够提供后台让产品或运营来配置交互的方式和信息
- 统一:有的时候会遇到不一样的客户端,iOS、Android、前端对于交互的实现不统一的状况,若是API结果能够规定这部份内容能够完全避免这个问题
- 行为驱动:更深一层的服务端驱动,能够实现一套API做为入口,让客户端进行调用,而后经过约定一套DSL告知客户端应该呈现什么,干什么。
以前有两个这样的项目采用了相似的API设计方式:
- 贷款审核:咱们知道贷款的信用审核逻辑每每会变更比较大,还涉及到客户端的一些受权(好比运营商爬虫),并且App的发布更新每每比较困难(苹果App Store以及安卓各大应用商店的审核问题)。若是采用服务端驱动的架构来告知客户端接下去应该呈现什么界面作什么,那么会有很大的灵活性。
- 客户端爬虫:咱们知道若是采用服务端作爬虫不少时候由于IP的问题会被封,因此须要找不少代理。某项目咱们想出了客户端共享代理的概念,使用手机客户端来作分布式代理,由服务端驱动调度全部的客户端,那么这个时候客户端须要遵从服务端的指示来作请求而后上报响应。
通常而言,对外的Web API是不会采用这种服务端驱动客户端的方式来设计API的。对于某些特殊类型的项目,咱们能够考虑采用这种服务端驱动的方式来设计API,让客户端变为一个不含逻辑的执行者,执行的是UI和交互。
选择哪一个模式
https://user-gold-cdn.xitu.io/2019/2/15/168eff296f015115 此文给出了一个有关RPC、REST、GRAPHQL选择的决策方式能够参考,见上图。
我以为:
- 在下列状况考虑RPC风格的API或说是RPC:
- 偏向内部的API
- 没有太多的时间考虑API的设计或没有架构师
- 提供的API很难进行资源、对象抽象
- 对性能有高要求
- 在下列状况考虑REST风格:
- 偏向外部API
- 提供的API天生围绕资源、对象、管理展开
- 不能耦合客户端实现
- 在下列状况考虑GraphQL:
- 客户端对于数据的需求多变
- 数据具备图的特色
- 在下列状况考虑服务端驱动:
- 客户端发版更新困难,须要极端的灵活性控制客户端
- 仅限私有API
更多须要考虑的设计点
不少API设计指南都提到了下面这些设计考量点,也须要在设计的时候进行考虑:
- 版本控制,好比:
- 经过URI Path进行版本控制,好比https://adventure-works.com/v2/customers/3
- 经过QueryString进行版本控制,好比https://adventure-works.com/customers/3?version=2
- 经过Header进行版本控制,好比加一个请求头api-version=1
- 经过Media Type进行版本控制,好比Accept: application/vnd.adventure-works.v1+json
- 缓存策略,好比:
- 响应使用Cache-Control告知客户端缓存时间(max-age)、策略(private、public)
- 响应使用ETag来进行资源版本控制
- 部分响应:好比大的二进制文件须要考虑实现HEAD Method来代表资源容许分段下载,以及提供资源大小信息:
HEAD https://adventure-works.com/products/10?fields=productImage HTTP/1.1 HTTP/1.1 200 OK Accept-Ranges: bytes Content-Type: image/jpeg Content-Length: 4580
而后提供资源分段下载功能:
GET https://adventure-works.com/products/10?fields=productImage HTTP/1.1 Range: bytes=0-2499 HTTP/1.1 206 Partial Content Accept-Ranges: bytes Content-Type: image/jpeg Content-Length: 2500 Content-Range: bytes 0-2499/4580 [...]
- 列表设计:须要在设计列表类型API的时候考虑分页、投影、排序、查询几点,值得注意的是列表API的额外功能比较多,尽可能进行命名的统一化规范
参考资料
- 微软API设计指南:https://docs.microsoft.com/zh-cn/azure/architecture/best-practices/api-design (英文版: https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-implementation )
- Google Cloud API设计指南: https://google-cloud.gitbook.io/api-design-guide/ (英文版:https://cloud.google.com/apis/design/ )
- Github API概览:https://developer.github.com/v3/