后台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) 调用代理方法, ...
- Android 常见工具类封装
1,MD5工具类: public class MD5Util { public final static String MD5(String s) { char hexDigits[] = { '0' ...
- 使用SignalR构建一个最基本的web聊天室
What is SignalR ASP.NET SignalR is a new library for ASP.NET developers that simplifies the process ...
- AutoMapper配置方法
在Mvc开发中,我们经常需要构建一个viewModel出来供页面使用,在PO和VO之间相互传值的时候,如果实体字段比较多的时候,那么传值将变得异常麻烦,也使得代码非常的臃肿.AutoMapper可以帮 ...
- 2014 Asia AnShan Regional Contest --- HDU 5078 Osu!
Osu! Problem's Link: http://acm.hdu.edu.cn/showproblem.php?pid=5078 Mean: 略. analyse: 签到题,直接扫一遍就得答 ...
- C++隐藏规则
在面向对象的开发过程中,经常出现类的继承,这里面出现的成员函数的重载(overload).覆盖(override)与隐藏(hidden)很容易混淆. 首先澄清这3个概念: 重载 相同的范围(在同一个类 ...
- 【jQuery基础学习】04 jQuery中的表格操作及cookie插件的使用
这章本来准备写成jQuery的表单操作和表格操作的. 然而昨天吧jQuery的表单操作看完,发现全部在炒之前章节的剩饭,所以就没写出来. 那么今天就来看看表格吧. 因为平常做的都是公司的内部管理系统, ...
- bzoj1146整体二分+树链剖分+树状数组
其实也没啥好说的 用树状数组可以O(logn)的查询 套一层整体二分就可以做到O(nlngn) 最后用树链剖分让序列上树 #include<cstdio> #include<cstr ...
- DP---Mahjong tree
HDU 5379 Problem Description Little sun is an artist. Today he is playing mahjong alone. He suddenl ...
- js 倒计时 跳转
1. setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式. setTimeout() 只执行 code 一次.如果要多次调用,请使用 setInterval() 或者让 code ...