0. API设计满足关键点
- API应当基于 web 标准来设计
- API应当对开发者友好并且便于在浏览器地址栏中浏览和探索
- API应当是简单、直观和一致的,使它用起来方便和舒适
- API应当是高效的,同时要维持和其他需求之间的平衡
1. API命名
1.1 根地址
好的RESTful API要基于HTTPS来发布 API规模不大时,在域名后面增加 api 目录,如:https://www.trawe.cn/api/
API规模很大时,使用以api开头的二级域名,如:https://api.trawe.cn/
1.2 版本问题
-
新版本尽量对旧版本作兼容
-
版本信息放在URL中
https://api.trawe.cn/v1.2/users/123
- 协议报文中增加version字段
{ version: "1.0",.... .... }
- HTTP Header中增加版本信息
使用已的HTTPHeader:Accept Header:Accept: application/json+v1.2 自定义 Header: X-Api-Version: 1.2
1.3 端点设计原则
1) 命名
- CRUD操作一律使用名词,不使用动词
- url一律使用小写字母
- url命名方式不使用 camel方式,采用 - 连接两个单词,如:app-setups,而不是appSetups
- 请求参数命名方式使用 下划线 “_” 连接两个单词(Javascript规范),如:user_name,而不是userName或user-name
- 在url中不要出现 get / add / delete / put / modify / update 等动词,使用HTTP Method来替代
2) 无论对于单个资源还是集合,名词都使用复数形式,这样便于风格的统一
GET /tickets # 获取 tickets 列表 GET /tickets/12 # 获取一个单独的 ticket POST /tickets # 创建一个新的 ticket PUT /tickets/12 # 更新 ticket #12 PATCH /tickets/12 # 部分更新 ticket #12 DELETE /tickets/12 # 删除 ticket #12 GET /tickets/12/messages # 获取ticket #12下的消息列表 GET /tickets/12/messages/5 # 获取ticket #12下的编号为5的消息 POST /tickets/12/messages # 为ticket #12创建一个新消息 PUT /tickets/12/messages/5 # 更新ticket #12下的编号为5的消息 PATCH /tickets/12/messages/5 # 部分更新ticket #12下的编号为5的消息 DELETE /tickets/12/messages/5 # 删除ticket #12下的编号为5的消息
3) 对于非CRUD的操作 有很非CRUD服务,可以把这些服务看成资源,计算的结果是资源的presentation,按服务属性选择合适的HTTP方法。
1. 重新构造这个Action,使得它像一个资源的操作。 这种方法在Action不包含参数的情况下可以奏效。例如一个有效的action可以映射成布尔类型field,并且可以通过PATCH更新资源。 2. 利用RESTful原则像处理子资源一样处理它。 例如:Github的API让你通过PUT /gists/:id/star 来 star a gist ,而通过DELETE /gists/:id/star来进行 unstar 。 3. 有时候你实在是没有办法将Action映射到任何有意义的RESTful结构。 例如:多资源搜索没办法真正地映射到任何一个资源接入点。这种情况,/search 将非常有意义,虽然它不是一个名词,但是这样做没有问题,只需要从API消费者的角度做正确的事,并确保所做的一切都用文档清晰记录下来了即可。
4) 查询过滤 过滤: 对每一个字段使用一个唯一查询参数,就可以实现过滤。 例如: 当通过 /tickets
终端来请求一个票据列表时,我们需要增加一些限定来查询那些在售的票。可以使用 GET /tickets?state=open
这样的请求来实现。这里“state”是一个实现了过滤功能的查询参数。
对于常用的查询,有以下两种处理:
- 可以单独将查询包装为一个独立的API 如:
GET /trades?status=closed&sort=created,desc
可以包装GET /trades/recently-closed
- 查询结果标签化 将经常使用的、复杂的查询标签化,降低维护成本。如:
GET /trades?status=closed&sort=created,desc
可以标签化为GET /trades#recently-closed
排序: 跟过滤类似,使用排序参数字段来描述排序的规则。 为适应复杂排序需求,让排序参数采取逗号分隔的字段列表的形式,每一个字段前都可能有一个负号来表示按降序排序。 例如:
排序字段前面的 +表示升序 -表示降序 默认为升序 GET /tickets?sort=-priority # 获取票据列表,按优先级字段降序排序 GET /tickets?sort=-priority,created_at # 获取票据列表,按“priority”字段降序排序。在一个特定的优先级内,较早的票排在前面。
5) 减少层级深度 /
在url中表达层级,用于按实体关联关系进行对象导航,一般根据id导航。 过深的导航容易导致url膨胀,不易维护,如 GET /zoos/1/areas/3/animals/4
,尽量使用查询参数代替路径中的实体导航,如GET /animals?zoo=1&area=3
6) 限制返回哪些字段 使用一个字段查询参数,它包含一个 用逗号隔开的字段列表。例如,下列请求获得的信息将刚刚足够展示一个在售票的有序列表: GET /tickets?fields=id,subject,customer_name,updated_at&state=open&sort=-updated_at
7) 关于返回结果 通常情况下只返回JSON
格式结果。 返回结果支持gzip压缩:进行gzip压缩的数据可节省50%以上的带宽 对于需要返回不同格式资源的情况:
- 在HTTP Header中指定格式 如:
Accept:application/xml;q=0.6,application/atom+xml;q=1.0
- URL后缀增加扩展名: 如:
/users/1.xml
limit=10:指定返回记录的数量 offset=10:指定返回记录的开始位置。 page=2&per_page=100:指定第几页,以及每页的记录数。
9) 缓存 ETag: 当产生一个请求时,包含一个HTTP 头,ETag会在里面置入一个和表达内容对应的哈希值或校验值。这个值应当跟随表达内容的变化而变化。现在,如果一个入站HTTP请求包含了一个If-None-Match头和一个匹配的ETag值,API应当返回一个304未修改状态码,而不是返回请求的资源。 Last-Modified: 基本上像ETag那样工作,不同的是它使用时间戳。在响应头中,Last-Modified包含了一个RFC 1123格式的时间戳,它使用If-Modified-Since来进行验证。注意,HTTP规范已经有了 3 种不同的可接受的日期格式 ,服务器应当准备好接收其中的任何一种。
2. HTTP方法
常用:
GET (选择):从服务器上获取一个具体的资源或者一个资源列表。 POST (创建): 在服务器上创建一个新的资源。 PUT (更新):以整体的方式更新服务器上的一个资源。 PATCH (更新):只更新服务器上一个资源的一个属性。 DELETE (删除):删除服务器上的一个资源。
不常用:
HEAD : 获取一个资源的元数据,如数据的哈希值或最后的更新时间。 OPTIONS:获取客户端能对资源做什么操作的信息。
3. 数据报文
- 统一报文格式
请求使用JSON格式时: { method: 'testMethod',// 请求方法名称 version: '1.0',// 接口版本 token: '0X十六进制',// Token sign_type: 'MD5',// 签名算法 sign: '0X十六进制',// 签名 timestamp: '12345678' // 请求的时间戳 } 响应: { code: 0,// 返回码。 -1:失败 0:成功 其它:具体业务代码 message: '处理成功',// 返回码描述信息 sign: '0X十六进制',// 签名 ... // 业务数据 }
响应数据不作多余包装,如下为错误示例:
{ code: 0,data: {userName: "用户名"} // 这里面的data没有业务含意,仅仅是为了包装,所以应该去掉 }
需要进行包装的情况:
- 使用JSONP进行跨域请求
- 当客户端没有能力处理HTTP头信息时
- 对于日期类型的字段处理
- 一种方式为:转为一定格式的字符串,如 yyyy-MM-dd HH:mm:ss.SSS
- 另一种方式为:转为长整型数字时间戳
4. 安全
- 使用Https传输数据
- 使用Token(如JWT)来标识用户状态并设置失效时间
- Token失效后客户端自动重新登录获取新的Token
- 发送请求时对参数按ASCII排序计算签名(Hash算法或对称加密算法,可以所有接口统一密钥,也可以一个接口一个密钥),接收到请求后先验证签名
- 每个端(Android、iOS、微信服务号、Web网站)生成一个AppKey
- 不设置密码,登录时使用手机+验证码方式登录
5. 文档
- 文档须提供从请求到响应整个循环的示例;
- 请求应该是可粘贴的例子,要么是可以贴到浏览器的链接,要么是可以贴到终端里的curl示例 ;
- 一旦发布一个公开的API,必须承诺 在没有通知的前提下,不会更改API的功能;
- 对于外部可见API的更新,文档必须包含任何将废弃的API的时间表和详情;
6. HTTP状态代码
HTTP定义了一套可以从API返回的有意义的状态代码。 这些代码能够用来帮助API使用者对不同的响应做出相应处理。
200 OK (成功) - 对一次成功的GET,PUT,PATCH 或 DELETE的响应。也能够用于一次未产生创建活动的POST 201 Created (已创建) - 对一次导致创建活动的POST的响应。 同时结合使用一个位置头信息指向新资源的位置 204 No Content (没有内容) - 对一次没有返回主体信息(像一次DELETE请求)的请求的响应 304 Not Modified (未修改) - 当使用HTTP缓存头信息时使用304 400 Bad Request (错误的请求) - 请求是畸形的,比如无法解析请求体 401 Unauthorized (未授权) - 当没有提供或提供了无效认证细节时。如果从浏览器使用API,也可以用来触发弹出一次认证请求 403 Forbidden (禁止访问) - 当认证成功但是认证用户无权访问该资源时 404 Not Found (未找到) - 当一个不存在的资源被请求时 405 Method Not Allowed (方法被禁止) - 当一个对认证用户禁止的HTTP方法被请求时 410 Gone (已删除) - 表示资源在终端不再可用。当访问老版本API时,作为一个通用响应很有用 415 Unsupported Media Type (不支持的媒体类型) - 如果请求中包含了不正确的内容类型 422 Unprocessable Entity (无法处理的实体) - 出现验证错误时使用 429 Too Many Requests (请求过多) - 当请求由于访问速率限制而被拒绝时
7. 错误处理
原则
- 不要发生了错误但给2xx响应,客户端可能会缓存成功的http请求;
- 正确设置http状态码,不要自定义;
- Response body 提供 1) 错误的代码(日志/问题追查);2) 错误的描述文本(展示给用户)。
API 可能抛出两类异常:业务异常和非业务异常。 业务异常由自己的业务代码抛出,表示一个用例的前置条件不满足、业务规则冲突等,比如参数校验不通过、权限校验失败。 非业务类异常表示不在预期内的问题,通常由类库、框架抛出,或由于自己的代码逻辑错误导致,比如数据库连接失败、空指针异常、除0错误等等。
业务类异常必须提供2种信息:
- 如果抛出该类异常,HTTP 响应状态码应该设成什么;
- 异常的文本描述;
在Controller层使用统一的异常拦截器:
- 设置 HTTP 响应状态码:对业务类异常,用它指定的 HTTP code;对非业务类异常,统一500;
- Response Body 的错误码:异常类名
- Response Body 的错误描述:
- 对业务类异常,用它指定的错误文本;
- 对非业务类异常,线上可以统一文案如“服务器端错误,请稍后再试”;
- 开发或测试环境中用异常的 stacktrace,服务器端提供该行为的开关。
8. 超媒体API
RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。 比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。
{"link": { "rel": "collection https://www.example.com/zoos","href": "https://api.example.com/zoos","title": "List of zoos","type": "application/vnd.yourformat+json" }}
上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。 Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。
{ "current_user_url": "https://api.github.com/user","authorizations_url": "https://api.github.com/authorizations",// ... }
从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。
{ "message": "Requires authentication","documentation_url": "https://developer.github.com/v3" }