后台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-UIScrollView-图片缩放
一. 实现功能 两个手指捏合,可以放大或者缩小图片. 二.原理说明 1. 实现缩放功能的四个步骤 (1) 让控制器遵守代理协议 (2) 让scrollView设置代理 self (3) 调用代理方法, ...
- 看看如何面试前端工程师:Github很重要
从程序员的角度提出要去学习哪些知识,下面这篇文章从面试官的角度介绍到面试时可能会问到的一些问题.不过我想先给你们一个忠告,招聘是一件非常艰巨的任务,在45分钟内指出一名侯选人是否合适是你需要完成的任务 ...
- 0525Sprint回顾
1.回顾组织 主题:“我们下次怎么样才能更加认真对待?” 时间:设定为1至2个小时. 参与者:整个团队. 场所:能够在不受干扰的情况下讨论. 秘书:指定某人当秘书,筹备.记录.整理. 2.回顾流程 ...
- ASP.NET运行时详解 集成模式和经典模式
遗留问题 在<ASP.NET运行时详解 生命周期入口分析>中遗留两个问题,包括Application的InitInternal方法执行细节.IIS6和II7经典模式请求管道管理类Appli ...
- 给文本框添加模糊搜索功能(“我记录”MVC框架下实现)
步骤: 1.在文本框中输入内容时,触发keyup事件: 2.在keyup事件的处理方法中,通过Ajax调用控制器的方法: 3.在控制器方法中,搜索满足条件的数据,这里分页获取数据,且只取第一页的数据, ...
- web api返回格式小结
web api返回格式小结: 1.默认是返回xml格式数据,如果需要返回json格式,需要在Global.asax中加入: GlobalConfiguration.Configuration.Form ...
- ASP.NET使用UpdatePanel实现AJAX
ScriptManager和UpdatePanel控件联合使用可以实现页面异步局部更新的效果.其中的UpdatePanel就是设置页面中异 步局部更新区域,它必须依赖于ScriptManager存在, ...
- [CLR via C#]25. 线程基础
一.Windows为什么要支持线程 Microsoft设计OS内核时,他们决定在一个进程(process)中运行应用程序的每个实例.进程不过是应用程序的一个实例要使用的资源的一个集合.每个进程都赋予了 ...
- Python入门笔记(13):列表解析
一.列表解析 列表解析来自函数式编程语言(haskell),语法如下: [expr for iter_var in iterable] [expr for iter_var in iterable i ...
- MySQL常用SQL语句
一.数据库操作 1.创建数据库: Mysql> CREATE DATABASE databaseName; Mysql数据库的数据文件默认存放在/usr/local/mysql/var/目录下, ...