我在《写在最前》里说过,后台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服务的设计考虑的更多相关文章

  1. 开放接口/RESTful/Api服务的设计和安全方案

    总体思路 这个涉及到两个方面问题:一个是接口访问认证问题,主要解决谁可以使用接口(用户登录验证.来路验证)一个是数据数据传输安全,主要解决接口数据被监听(HTTPS安全传输.敏感内容加密.数字签名) ...

  2. 部署基于.netcore5.0的ABP框架后台Api服务端,以及使用Nginx部署Vue+Element前端应用

    前面介绍了很多关于ABP框架的后台Web API 服务端,以及基于Vue+Element前端应用,本篇针对两者的联合部署,以及对部署中遇到的问题进行处理.ABP框架的后端是基于.net core5.0 ...

  3. 开放接口/RESTful/Api服务的设计和安全方案详解

    一.总体思路 这个涉及到两个方面问题:一个是接口访问认证问题,主要解决谁可以使用接口(用户登录验证.来路验证)一个是数据数据传输安全,主要解决接口数据被监听(HTTPS安全传输.敏感内容加密.数字签名 ...

  4. NodeJs+Express+SqlServer简易后台API服务搭建

    首先安装nodejs 第一步 创建node项目配置package.json如下 express 使用方法可参考http://www.runoob.com/nodejs/nodejs-express-f ...

  5. 微服务从设计到部署(二)使用 API 网关

    链接:https://github.com/oopsguy/microservices-from-design-to-deployment-chinese 译者:Oopsguy 本书的七个章节是关于设 ...

  6. API服务接口签名代码与设计,如果你的接口不走SSL的话?

    在看下面文章之前,我们先问几个问题 rest 服务为什么需要签名? 签名的几种方式? 我认为的比较方便的快捷的签名方式(如果有大神持不同意见,可以交流!)? 怎么实现验签过程 ? 开放式open ap ...

  7. .net core实践系列之短信服务-架构设计

    前言 上篇<.net core实践系列之短信服务-为什么选择.net core(开篇)>简单的介绍了(水了一篇).net core.这次针对短信服务的架构设计和技术栈的简析. 源码地址:h ...

  8. .net core实践系列之短信服务-Sikiro.SMS.Api服务的实现

    前言 上篇<.net core实践系列之短信服务-架构设计>介绍了我对短信服务的架构设计,同时针对场景解析了我的设计理念.本篇继续讲解Api服务的实现过程. 源码地址:https://gi ...

  9. REST API权限集成设计

    REST API权限集成设计 应用分为两大部分,前端html+后端Rest服务,前端html和后端Rest服务部署完全分离. 目标:可访问资源都处于权限控制之下(意味着通过浏览器地址栏的任意url都会 ...

随机推荐

  1. iOS-UIScrollView-图片缩放

    一. 实现功能 两个手指捏合,可以放大或者缩小图片. 二.原理说明 1. 实现缩放功能的四个步骤 (1) 让控制器遵守代理协议 (2) 让scrollView设置代理 self (3) 调用代理方法, ...

  2. Android 常见工具类封装

    1,MD5工具类: public class MD5Util { public final static String MD5(String s) { char hexDigits[] = { '0' ...

  3. 使用SignalR构建一个最基本的web聊天室

    What is SignalR ASP.NET SignalR is a new library for ASP.NET developers that simplifies the process ...

  4. AutoMapper配置方法

    在Mvc开发中,我们经常需要构建一个viewModel出来供页面使用,在PO和VO之间相互传值的时候,如果实体字段比较多的时候,那么传值将变得异常麻烦,也使得代码非常的臃肿.AutoMapper可以帮 ...

  5. 2014 Asia AnShan Regional Contest --- HDU 5078 Osu!

    Osu! Problem's Link:   http://acm.hdu.edu.cn/showproblem.php?pid=5078 Mean: 略. analyse: 签到题,直接扫一遍就得答 ...

  6. C++隐藏规则

    在面向对象的开发过程中,经常出现类的继承,这里面出现的成员函数的重载(overload).覆盖(override)与隐藏(hidden)很容易混淆. 首先澄清这3个概念: 重载 相同的范围(在同一个类 ...

  7. 【jQuery基础学习】04 jQuery中的表格操作及cookie插件的使用

    这章本来准备写成jQuery的表单操作和表格操作的. 然而昨天吧jQuery的表单操作看完,发现全部在炒之前章节的剩饭,所以就没写出来. 那么今天就来看看表格吧. 因为平常做的都是公司的内部管理系统, ...

  8. bzoj1146整体二分+树链剖分+树状数组

    其实也没啥好说的 用树状数组可以O(logn)的查询 套一层整体二分就可以做到O(nlngn) 最后用树链剖分让序列上树 #include<cstdio> #include<cstr ...

  9. DP---Mahjong tree

    HDU  5379 Problem Description Little sun is an artist. Today he is playing mahjong alone. He suddenl ...

  10. js 倒计时 跳转

    1. setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式. setTimeout() 只执行 code 一次.如果要多次调用,请使用 setInterval() 或者让 code ...