后台API服务的设计考虑
我在《写在最前》里说过,后台API的文档至关重要。不过,文档只是外在表现形式,设计才是真正的灵魂。我在这篇博文主要介绍的就是我在后台开发过程中,设计API时的考虑。我只说他是考虑,因为很多东西未必是正确的,更不会是绝对了。
首先,我要声明的是,我主要是参考下面这篇文章(以下简称最佳实践)里的理念:
http://www.cnblogs.com/yuzhongwusan/p/3152526.html [标题: RESTful API 设计最佳实践]
这是一篇翻译过来的博文,原文地址是:
http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
下面是我基于那篇文章的一些补充;也就是说,读者得先读一读那一篇:
1. 我的接口设计中并没有形如/tickets/12/messages/5的接口,因为在我的资源管理中,id是全局唯一的,不需要使用12-5这样的联合id。我的替代方案是:/ticket_messages/112255
2. 就像最佳实践博文里的所提的那样,我的List接口中也使用了诸如sort、分页等机制,搜索这块也使用了简明的q参数。我还加入了include和with_total, just_total等控制。
include: 这是解决典型的N+1问题的方案。例如我们定义Ticket资源,同时每个Ticket资源绑定到(belongs_to)一个User资源。通过调用接口/tickets可以访问Ticket列表。此时只会返回Ticket的信息,而不会嵌入Ticket的所有者User的信息,只是嵌入一个user的id字段,如下:
- [
- {
- ...ticket1...,
- user_id: 1
- },
- {
- ...ticket2...,
- user_id: 2
- },
- ...
- ]
这里会返回N个Ticket,调用了一次接口。另外,如果想要进一步知道每个Ticket的User的信息,需要调用N次/users/:user_id接口,如下:
/users/1
/users/2
...
/users/N
这就是著名的N+1问题。这样不仅浪费带宽,而且也可能会影响数据库查询效率。更重要的是,这无疑增加了前端的工作量。使用include参数的模式是在接口调用中声明要包含的子资源,通过一次接口调用实现N+1此接口调用的效果。调用/tickets?include=user,返回的形式是:
- [
- {
- ...ticket1...,
- user: {
- id: 1,
- ...user1...
- }
- },
- {
- ...ticket2...,
- user: {
- id: 2,
- ...user2...
- }
- },
...- ]
这个就是我们后台API目前的设计形式。其中include字段可以配置包含多个相关资源,用逗号分隔即可。
再说明以下这个设计的一个小缺陷。就是出现不一致的情况。如果返回的结果存储在tickets中,那么第一个Ticket是tickets[0],那么没有设置include参数时,取user的id是tickets[0].user_id;当设置include=user时,取user的id是tickets[0].user.id。在取Ticket的User的id时,出现了调用的不一致,不好。
很抱歉,我居然没有意识到《最佳实践》里已经有个类似字段embed了。有些概念讲重复了。
with_total, just_total:一般来说,我在设计"列出..."的API接口时,仅仅返回的是你需要的数组。这里面包含能够匹配的资源的总数信息。最常用的情形是做分页的时候,这个总数信息可以导出总页数,进而判断当前的请求页是否是最后一页,从而在此时能够很好地灰选下一页按钮。我一开始不想添加这样的功能,因为我认为用瀑布流刷数据是更酷的方式,这时候通过返回一个空数组指示没有更多数据即可。仅仅返回一个数组,便于前端开箱即用返回数据,也算是一个小小的便利吧。最后还是添加了这个功能,毕竟全部瀑布流的方式并不能完全适应前端的设计,而且有时候确实需要知道一个新闻的关注总量这样的信息。这时候添加两个控制参数with_toal, just_total,它们的使用是互斥的。它们是控制字段,不用显示赋值为true,只要这个字段存在即可。
例如/tickets?with_total, /tickets?with_total=true, /tickets?with_total=false都是返回如下结果(总数存储在total字段中,数据数组存储在list字段中):
- {
- total: 1234,
- list: [
- ...
- ]
- }
just_total仅仅返回总数信息,抹去了list字段,这个时候就不要做一些分页控制了,因为没效果。即:
- {
- total: 1234
- }
3. 筛选控制
所谓筛选,是指通过条件查询返回匹配的资源。例如只返回address在’上海‘的User资源。《最佳实践》中是用一个类似'address=上海'这样的参数来控制条件查询。我没有采用这种分散的方式,而是把所有的查询相关条件都定义在cond这一个参数中。在上面的例子中,大致应该是cond={address: '上海'}。注意这里我用了javascript语法已减少输入,您可以把它看成等效的JSON语法。也就是说,cond参数的类型应该是一个对象,在其中定义所有的查询逻辑。更具体地说,cond参数应该是一系列的键值对,键是要配置的字段名,如address,值是匹配的规则,如设置成'上海',就是说这个字段要完全等于'上海'这个字符串。例如在'address=上海&age=18'这样的多字段匹配的情况下,用cond语法就应该是cond={address: '上海', age: 18}。现在在我的项目里我确实也只做了这些,诸如其他的age>18,age<18都没有实现。好在现在项目规模很小,还没用到这些。
将所有查询控制封装在一个cond参数中方便接口的统一性,也便于我统一实现。
不过,接下来我重点要说的是,这里面有坑。此坑在于,GET方法(上面所说的那些是用于HTTP GET METHOD)中,没有请求体,也就不能指定Content-Type=application/json。在cond中定义一个键值对集合,用json是很自然的方式,不过不能传递json。GET方法中如果要传递参数,只能附加在url中的query string当中。我不确定能不能传递json格式的数据,但我实际没有成功过。一般来说,query string的格式是field1=value1&field2=value2&...这样的形式。有一个不成文的约定,当传递数组时,使用array[]=v1&array[]=v2&...这样的形式,当传递对象,例如上面的cond中的对象,形式是cond[address]=上海&cond[age]=18. 这个约定不是放之四海皆准。项目的前端使用的是AngularJS,如果调用Resource的方法时,将下面的对象作为请求参数(我感觉这样对前端来说是最自然的),会让后端收到的query string那段非常地不正常:
- {
..., //首先可以有分页,排序等参数- cond: {
- address: '上海',
- age: 18
- }
- }
最后前端使用了一种不自然的调用方式,是下面:
- {
..., //首先可以有分页,排序等参数- 'cond[address]': '上海',
- 'cond[age]': 18
- }
我在考虑下面两种解决方案:
将cond参数的对象转化成JSON字符串,这样
- { ..., //首先可以有分页,排序等参数
- cond: JSON.parse({
- address: '上海',
- age: 18
- })
- }
或者使用POST方法来查询数据,接口是POST /users/query 查询条件可以用json格式写在请求体中。
前一种方式并不能让我满意,后一种实现起来较为繁琐。要处理GET /users和POST /users/query两种情况。
4. 错误返回
我的设计里是包含出错情况的。当需要返回用户一个出错消息的时候,首先会返回一个状态码。就像《最佳实践》里面说的,有些是我必然用到的,它们是:
- 400 Bad Request (错误的请求) - 请求是畸形的, 比如无法解析请求体
- 401 Unauthorized (未授权) - 当没有提供或提供了无效认证细节时。如果从浏览器使用API,也可以用来触发弹出一次认证请求
- 403 Forbidden (禁止访问) - 当认证成功但是认证用户无权访问该资源时
- 404 Not Found (未找到) - 当一个不存在的资源被请求时
- 422 Unprocessable Entity (无法处理的实体) - 出现验证错误时使用
- 500 Internal Server Error (服务器内部错误) - 服务器总会出现未知错误的
相信随着项目的发展,更多的状态码会用起来。
接着返回体中会返回一个json格式的错误消息。它的样子是:
- {
- message: 'something is wrong'
- }
不过,我更想要的样子是:
- {
- code: code, //更细致的错误编号,必要时提供
- error: error, //错误消息,面向API的调用者,必要时提供
- message: message //错误消息,此消息可以让前端使用alert之类的方法显示给终端用户,必要时提供
- }
做这个更改,是因为我发现前端有时需要判断错误的类型以做出相应的动作。另外,有时出错消息可以很轻易地归纳出面向终端用户的提示消息,但有时又只能提供给API的调用者一个宽泛的错误提示。并且,API调用者和用户的关注点是不一样的。
后台API服务的设计考虑的更多相关文章
- 开放接口/RESTful/Api服务的设计和安全方案
总体思路 这个涉及到两个方面问题:一个是接口访问认证问题,主要解决谁可以使用接口(用户登录验证.来路验证)一个是数据数据传输安全,主要解决接口数据被监听(HTTPS安全传输.敏感内容加密.数字签名) ...
- 部署基于.netcore5.0的ABP框架后台Api服务端,以及使用Nginx部署Vue+Element前端应用
前面介绍了很多关于ABP框架的后台Web API 服务端,以及基于Vue+Element前端应用,本篇针对两者的联合部署,以及对部署中遇到的问题进行处理.ABP框架的后端是基于.net core5.0 ...
- 开放接口/RESTful/Api服务的设计和安全方案详解
一.总体思路 这个涉及到两个方面问题:一个是接口访问认证问题,主要解决谁可以使用接口(用户登录验证.来路验证)一个是数据数据传输安全,主要解决接口数据被监听(HTTPS安全传输.敏感内容加密.数字签名 ...
- NodeJs+Express+SqlServer简易后台API服务搭建
首先安装nodejs 第一步 创建node项目配置package.json如下 express 使用方法可参考http://www.runoob.com/nodejs/nodejs-express-f ...
- 微服务从设计到部署(二)使用 API 网关
链接:https://github.com/oopsguy/microservices-from-design-to-deployment-chinese 译者:Oopsguy 本书的七个章节是关于设 ...
- API服务接口签名代码与设计,如果你的接口不走SSL的话?
在看下面文章之前,我们先问几个问题 rest 服务为什么需要签名? 签名的几种方式? 我认为的比较方便的快捷的签名方式(如果有大神持不同意见,可以交流!)? 怎么实现验签过程 ? 开放式open ap ...
- .net core实践系列之短信服务-架构设计
前言 上篇<.net core实践系列之短信服务-为什么选择.net core(开篇)>简单的介绍了(水了一篇).net core.这次针对短信服务的架构设计和技术栈的简析. 源码地址:h ...
- .net core实践系列之短信服务-Sikiro.SMS.Api服务的实现
前言 上篇<.net core实践系列之短信服务-架构设计>介绍了我对短信服务的架构设计,同时针对场景解析了我的设计理念.本篇继续讲解Api服务的实现过程. 源码地址:https://gi ...
- REST API权限集成设计
REST API权限集成设计 应用分为两大部分,前端html+后端Rest服务,前端html和后端Rest服务部署完全分离. 目标:可访问资源都处于权限控制之下(意味着通过浏览器地址栏的任意url都会 ...
随机推荐
- iOS性能优化之内存管理:Analyze、Leaks、Allocations的使用和案例代码
最近接了个小任务,和公司的iOS小伙伴们分享下instruments的具体使用,于是有了这篇博客...性能优化是一个很大的话题,这里讨论的主要是内存泄露部分. 一. 一些相关概念 很多人应该比较了解这 ...
- WatiN框架学习二——对弹窗的处理
以IE为例,WatiN处理弹出窗口: IE ie = new IE("string"); //打开指定web页 ie.Button(Find.ById("string&q ...
- Sprint第三个冲刺(第三天)
一.Sprint介绍 任务进度: 二.Sprint周期 看板: 燃尽图:
- 转载---QRcodeJS生成二维码
QRCode.js QRCode.js是依赖JS生成二维码的.主要是通过获取DOM的标签,再通过HTML5Canvas绘制而成,不依赖JQ 获取QRCode.js Github-Page:qrcode ...
- c++转C#
//c++:HANDLE(void *) ---- c#:System.IntPtr //c++:Byte(unsigned char) ---- ...
- EasyUI组合树插件
一.引用CSS和JS <link href="~js/easyui/easyui.css" rel="stylesheet" type="tex ...
- 【C#】委托
一.委托的基本的写法 internal class Program { private static void Main(string[] args) { ChainDelegate(); Conso ...
- Windows Azure开发者任务之五:配置虚拟机的“规模”
指定虚拟机的“规模”是怎么一回事? 我们可以指定角色将要部署于其上的虚拟机的“规模”.虚拟机的“规模”是指: 1,CPU核心数 2,内存容量 3,本地文件系统的体积 我们可以针对具体的角色来指定虚拟机 ...
- WPF 程序自删除(自毁)|卸载程序删除
一般是在MainWindow_Closed 事件中调用批处理命令删除. 在借鉴别人的想法的基础上的算是改进. 自删除步骤: 1.删除文件 2.删除存放文件夹. 实现代码: private static ...
- ubuntu 14.04 64位安装bigbluebutton
BigBlueButton 是一个使用 ActionScript 开发的在线视频会议系统或者是远程教育系统,主要功能包括在线PPT演示.视频交流和语音交流,还可以进行文字交流.举手发言等功能,特别适合 ...