API接口
API接口
以下为学习过程中的极简提炼笔记,以供重温巩固学习
学习准备
准备工作
学习目的
认识API接口各术语概念
接口是什么
定义:
- 接口是
前后端通信的桥梁
- 前后端之间,依靠
API 接口
实现数据交互/通信
- 接口是
简单理解:
- 一个接口就是
服务中的一个路由规则
,根据请求响应结果 - 该接口/路由规则,能接收客户端发出的请求,并给客户端响应结果
- 一般在项目中,接口给客户端返回结果时,数据一般是JSON格式
- 一个接口就是
接口的英文单词是 API (Application Program Interface),所以有时也称之为
API 接口
这里的接口指的是 数据接口 ,
与编程语言(Java,Go 等)中的接口语法/接口结构不同
接口的作用
- 接口的作用:实现
前后端通信
接口的开发与调用
接口开发:
- 大多数接口都是由
后端工程师
开发的,开发语言不限
- 可以使用不同的技术去开发搭建后端服务,如Java、go、nodejs、js
- 大多数接口都是由
接口调用:
- 一般情况下接口都是由
前端工程师
调用的, - 但有时
后端工程师也会调用接口
,比如短信接口,支付接口等- 某些引入第三方接口的场景,需要后端去调用
- 一般情况下接口都是由
接口的组成
一个接口一般由如下几个部分组成(类似路由规则组成)
请求方法(传参)
接口地址(URL)
请求参数
响应结果(返回结果是JSON)
接口文档:
- 意义:对接口的使用作说明,在使用时需要如何请求,以及提供响应格式
- 后端工程师需要将接口的功能实现,并提供接口文档
- 前端工程师根据接口文档调用接口,获取数据
一个接口示例 https://www.free-api.com/doc/325
体验一下:https://api.asilu.com/idcard/?id=371522199111299668
RESTful API
RESTful API 是一种特殊风格的接口
,主要特点有如下几个:
URL 中的路径表示
资源
,路径中不能有动词
,例如create
,delete
,update
等这些都不能有操作资源的方法,要与
HTTP 请求方法
对应- 操作类型与请求类型对应
操作结果要与
HTTP 响应状态码
对应
规则示例:
操作 | 请求类型 | URL | URL属性 | 返回 |
---|---|---|---|---|
新增歌曲 | POST | /song | 不含id标识 | 返回新生成的歌曲信息 |
删除歌曲 | DELETE | /song/10 | 带id标识:10 | 返回一个空文档 |
修改歌曲 | PUT | /song/10 | 带id标识:10 | 返回更新后的歌曲信息 |
修改歌曲(部分字段) | PATCH | /song/10 | 带id标识:10 | 返回更新后的歌曲信息 |
获取所有歌曲 | GET | /song | 不含id标识 | 返回歌曲列表数组 |
获取单个歌曲 | GET | /song/10 | 带id标识:10 | 返回单个歌曲信息 |
规则备注:
- PUT:完整更新
- 将文档完整/所有属性,更新为输入的属性(更新为传入的对象);
- 如输入的属性不全,则没有输入的部分将会被清空抛弃
- PATCH:局部更新
- 更新文档中的同名属性值,不改动其他字段(只更新传入的对象中的同名属性值)
扩展阅读:https://www.ruanyifeng.com/blog/2014/05/restful_api.html
附:RESTful API 设计指南 - 阮一峰的网络日志
网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷(手机、平板、桌面电脑、其他专用设备......)。
因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。这导致API构架的流行,甚至出现"API First"的设计思想。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。我以前写过一篇《理解RESTful架构》,探讨如何理解这个概念。
今天,我将介绍RESTful API的设计细节,探讨如何设计一套合理、好用的API。我的主要参考了两篇文章(1,2)。
一、协议
API与用户的通信协议,总是使用HTTPs协议。
二、域名
应该尽量将API部署在专用域名之下。
https://api.example.com
如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。
https://example.org/api/
三、版本(Versioning)
应该将API的版本号放入URL。
https://api.example.com/v1/
另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法。
四、路径(Endpoint)
路径又称"终点"(endpoint),表示API的具体网址。
在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。
一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。
举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。
- https://api.example.com/v1/zoos
- https://api.example.com/v1/animals
- https://api.example.com/v1/employees
五、HTTP动词
对于资源的具体操作类型,由HTTP动词表示。
常用的HTTP动词有下面五个(括号里是对应的SQL命令)。
- GET(SELECT):从服务器取出资源(一项或多项)。
- POST(CREATE):在服务器新建一个资源。
- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
- DELETE(DELETE):从服务器删除资源。
还有两个不常用的HTTP动词。
- HEAD:获取资源的元数据。
- OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。
下面是一些例子。
- GET /zoos:列出所有动物园
- POST /zoos:新建一个动物园
- GET /zoos/ID:获取某个指定动物园的信息
- PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
- PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
- DELETE /zoos/ID:删除某个动物园
- GET /zoos/ID/animals:列出某个指定动物园的所有动物
- DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物
六、过滤信息(Filtering)
如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。
下面是一些常见的参数。
- ?limit=10:指定返回记录的数量
- ?offset=10:指定返回记录的开始位置。
- ?page=2&per_page=100:指定第几页,以及每页的记录数。
- ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
- ?animal_type_id=1:指定筛选条件
参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。 比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。
七、状态码(Status Codes)
服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
状态码的完全列表参见这里。
八、错误处理(Error handling)
如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。
{ error: "Invalid API key" }
九、返回结果
针对不同操作,服务器向用户返回的结果应该符合以下规范。
- GET /collection:返回资源对象的列表(数组)
- GET /collection/resource:返回单个资源对象
- POST /collection:返回新生成的资源对象
- PUT /collection/resource:返回完整的资源对象
- PATCH /collection/resource:返回完整的资源对象
- DELETE /collection/resource:返回一个空文档
十、Hypermedia 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" }
上面代码表示,服务器给出了提示信息,以及文档的网址。
十一、其他
(1)API的身份认证应该使用OAuth 2.0框架。
(2)服务器返回的数据格式,应该尽量使用JSON,避免使用XML。
(完)
json-server (仿真后端接口的 RESTful API 服务)
json-server 本身是一个 JS 编写的工具包,可以快速搭建 RESTful风格的 RESTful API 服务
- 可通过npm安装到项目中
- 前端开发过程中,通过调用自搭建的、临时的、仿真后端接口的 RESTful API 服务,快速实现前端功能;最后项目上线再将接口地址切换到生产环境
- 支持自动生成数据id值,与mongoose类似
操作步骤:
- 全局安装
json-server
npm i -g json-server
- 创建 JSON 文件(db.json),编写基本结构
{ "song": [ { "id": 1, "name": "干杯", "singer": "五月天" }, { "id": 2, "name": "当", "singer": "动力火车" }, { "id": 3, "name": "不能说的秘密", "singer": "周杰伦" } ] }
以 JSON 文件所在文件夹作为工作目录
,执行如下命令json-server --watch db.json
默认监听端口为 3000
- 服务运行后,可实时往服务中/.json文件中,增加资源/表,保存.json文件文件后,服务会自动重启
接口测试工具
介绍几个接口测试工具
Apipost 测试接口
使用接口测试工具前的注意事项:
- 先保证接口/仿真接口跑起来:
- json-server服务运行起来了再去测试
- 测试的服务url地址正确、端口正确
- 自动化测试:
- 当使用apipost测试过接口以后,可以写上接口测试说明,后续按ctrl+s保存,后续可以点击保存的测试项,打开前次测试的状态,作快速测试
- 在接口测试工具中,通过设置测试环境,来更改测试的服务url地址前缀,实现测试环境的切换;
- 后续使用时,设置发送请求,就只需要写路径和参数即可
- 测试post发送请求时,数据一般在请求体中
- Header设置请求头参数
- Query设置查询字符串参数
- Body设置请求体参数
- none请求体设为空
- form-data表单形式的数据(也支持 表单形式的请求体,即Querystring查询字符串内容形式 数据)
- x-www-form-urlencoded是Querystring形式的请求体
- raw原生请求体(包括JSON格式请求体)
Apipost 公共参数与文档功能
公共参数
- 需求:当前测试的各个接口,要求在发送请求时,需要增加传参公共参数
- 解决:
- 手动往每个保存的接口测试项,增加参数字段
- 通过新建文件夹,放入需要增加公共参数的接口测试项,通过设置文件夹,给接口测试项设置公用参数
文档功能
- 需求:编写接口后,需要写接口文档
- 解决:通过分享文档,自动生成接口相关信息
postman
- 测试post发送请求时,参数如下:
- Params设置查询字符串
- Header设置请求头参数
- 设置了一系列默认请求头,测试时会自动发给服务端,不需要的话勾选去掉
- Body设置请求体参数(选项与Apipost的类似)
附:工具比较
Apipost和Apifox真实测评 - 程序员友橙
近期被Apipost和Apifox刷屏,恰好两个都用过,出一篇对比测评,各位可以根据自身情况参考一下,有什么不足之处还请各位大佬多多指教。
一、产品定位
这块可以直接看他们的官网,个人感觉两款产品从功能和定位上来说,没啥明显区别。 Apipost: ApiPost = Postman + Swagger + Mock 后端、前端、测试同时在线编辑,内容实时同步。 apifox: API 文档、API 调试、API Mock、API 自动化测试 Apifox = Postman + Swagger + Mock + JMeter
二、学习成本
Apipost: 这界面设计可以说是开箱即用了,新用户进去后有一个默认的示例项目,这点挺好的,比较方便理解。平常调试接口、写参数、分享文档这些高频重要操作都非常好找,而且实操起来也很好上手。 apifox: 和Apipost一样,Apifox也有一个示例项目。不过他是文档先行,想调试接口得先写份文档,操作起来有些繁琐,而且这个tab老跳总感觉不太稳定,想改参数还得来回切(顺便,这里提个小BUG,我这新用户一进去默认就是测试环境,最基本的接口发送都没成功,按理说示例项目不该有这样的问题,上线前你们都不测试的吗?)
三、分享文档
关于API文档,第一,分享过程要方便快捷;第二,分享文档要有权限设置;第三,文档本身必须清晰明了,美观规范;下面我就用这三个标准来总结一下两款的工具的优缺点。 Apipost: 他的分享功能就在主页面里,一个是单接口分享,一个是整个项目分享,一共需要两步就可以完成操作。而且写这篇文章的时候发现他还支持自定义分享,可以跨目录点选接口,这点挺不错的,分享起来很灵活。 apifox: 他这个功能藏得有点深,一开始我还以为不支持分享功能,找了找发现在左边菜单栏的下面置灰了。而且分享前也要先填一堆设置项,感觉Apifox整个软件的操作逻辑都是这样,你想做什么事情都得先填个问卷才行,实操起来有点难受。
再从我开头说的那三个角度,做一个两款工具的总结吧:
Apipost | Apifox | |
---|---|---|
分享过程 | 位置明显,两步分享,非常丝滑。评分:★★★★★ | 功能藏得太深,五步分享,按理说这种功能不该设计这么麻烦。评分:★★☆☆☆ |
分享权限 | 支持自定义公开/私密;支持设置文档有效期;支持自定义接口分享。评分:★★★★★ | 支持自定义公开/私密;支持设置文档有效期;支持自定义接口分享。评分:★★★★★ |
文档本身 | 界面美观,信息全面;有全局参数的相关设置,而且支持导出Markdown、HTML、Word三种格式,分享出去非常方便。评分:★★★★★ | 和Apipost差不多,界面美观度和规范性也挺好的,但似乎不支持导出,这点真的很不方便,希望能支持上。评分:★★★☆☆ |
四、离线使用
因为公司对网络限制比较严格,所以我在挑选API工具时,都会考虑到是否支持离线使用。以下是这两款工具在这方面的对比: Apipost:(支持离线使用) 未登录状态下,以及内网环境下,都可以正常进行大部分操作。离线调试接口都是最基本的了,而且还能分享文档,个人感觉这点还是很不错的。 apifox:(不支持离线使用) 不支持离线,而且不登录没法用。
五、支持协议
协议 | Apipost | Apifox |
---|---|---|
http | ✔️ | ✔️ |
websocket | ✔️ | 不支持 |
grpc | ✔️ | 不支持 |
Apipost:Http + Websocket + Grpc(3种) http肯定是支持了,从Apipost官网上的更新日志来看,6.1.0版本就支持了websocket。我之前加了他们的用户群,群里有人提这个需求后,大概四月份websocket就上线了,感觉对用户非常上心。据说七月还会支持grpc,本人还是很期待的。 apifox:Http(1种) 目前似乎只支持http,用户群里也有人提这个问题,但官方没有正面回答,希望也能支持一下吧。
六、团队协作
Apipost: Apipost一直在宣传他们的协作功能,于是我拉了几个同事浅试了一下,感觉比想象中更好一些。一开始我以为的协作就是各做各的,然后接口互相覆盖,但其实他们已经做到实时协作这个程度了,而且让人眼前一亮的是协作人员的在线状态和权限也可以在工作台里展示,这点不管是对于管理层还是协作者来说都很方便,感觉Apipost还是很能站在用户角度想问题的。 apifox: 没有明显感觉到协作功能的体现,也不确定是不是自己没有找到(毕竟他们产品设计的总让人找不到地方),于是我就去他们官网查了一下,发现还是有一个协作板块的说明的,但看起来似乎价值不大,各个角色还是没有把协作的优势发挥出来,还是那句话,希望Apifox尽快完善一下自己的功能吧,目前来看差的还是有点多。
七、界面布局
Apipost: 试用了一下大概摸清了Apipost的布局逻辑,黄色框内是项目/全局设置,红色框内是接口设置,左边的绿色框内基本上是一个工作流的划分。个人感觉在实际工作中用起来很顺手,而且他的功能分区都是按实际使用场景分的,不会出现一个在工作步骤里需要跳转很多页面的情况,这点真的好评。
apifox: Apifox的工作流都是被一层一层的包裹住的,实际工作中想要找到某个功能还是比较困难的,以至于很多时候我都不确定某个功能是他没有还是我没找到,追溯问题时更是灾难现场,往往会浪费很多时间在排查错误步骤上。而且各个场景的功能项都没有捋清楚,浅试了一下他的自动化测试,感觉逻辑太绕了,还是那个“进行任何操作前,都要先填份问卷”的模式,真的应该改进一下,不然不仅是工作效率降低,上班的心情也很不好。
八、个人建议
总体而言,Apipost和Apifox各有优缺点,个人而言还是更喜欢Apipost,从产品本身来说,胜在功能全面,工作流逻辑清晰,支持多场景(离线)使用。从对用户的上心程度来说,我在Apipost用户群里经常看到他们老板半夜还在回答问题,而且复杂问题都会和用户连线远程解决,用户提出的需求也都会关注并且采纳(比如火速上线websocket),这一点还是比较打动人的,我也更看好这种真正关注用户的公司。
实战案例-记账本-结合api功能
接口就是把后端的回应链接拿来,获得执行功能后的data用于前端
由于有不同的前端,通过api,方便各个版本的客户端程序去访问、获取资源
结合api接口
目的:
- 通过api,方便各个版本的客户端程序去访问、获取资源
- 在此前的HTTP服务基础上,加上api功能,后续服务端程序开发完毕后,对接接口即可实现具体功能
- 接口返回的结果是JSON的,各端通过JSON对象,获取具体数据,再根据各端的前端框架/模板去渲染
步骤:
- 将网页端的路由规则,放到 项目文件夹\routes\web\index.js 路由文件中
- 复制一份网页端的路由规则,作为账单相关接口的路由规则,改放到 项目文件夹\routes\api\account.js 路由文件中
- 回到app.js中,导入独立的接口的路由规则文件 account.js,并加上路由路径
app.use('/api', accountRouter);
- 后续请求时,在请求路径上,通过加上路由路径/api,实现对账单相关接口的路由规则作访问
将网页端的路由规则,放到 项目文件夹\routes\web\index.js 路由文件中
var express = require('express');
var router = express.Router();
//导入 moment
const moment = require('moment');
const AccountModel = require('../../models/AccountModel');
//测试
// console.log(moment('2023-02-24').toDate())
//格式化日期对象
// console.log(moment(new Date()).format('YYYY-MM-DD'));
//记账本的列表
router.get('/account', function(req, res, next) {
//获取所有的账单信息
// let accounts = db.get('accounts').value();
//读取集合信息
AccountModel.find().sort({time: -1}).exec((err, data) => {
if(err){
res.status(500).send('读取失败~~~');
return;
}
//响应成功的提示
res.render('list', {accounts: data, moment: moment});
})
});
//添加记录
router.get('/account/create', function(req, res, next) {
res.render('create');
});
//新增记录
router.post('/account', (req, res) => {
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if(err){
res.status(500).send('插入失败~~');
return
}
//成功提醒
res.render('success', {msg: '添加成功哦~~~', url: '/account'});
})
});
//删除记录
router.get('/account/:id', (req, res) => {
//获取 params 的 id 参数
let id = req.params.id;
//删除
AccountModel.deleteOne({_id: id}, (err, data) => {
if(err) {
res.status(500).send('删除失败~');
return;
}
//提醒
res.render('success', {msg: '删除成功~~~', url: '/account'});
})
});
module.exports = router;
- 将账单相关接口的路由规则,放到 项目文件夹\routes\api\account.js
var express = require('express');
var router = express.Router();
//导入 moment
const moment = require('moment');
const AccountModel = require('../../models/AccountModel');
//测试
// console.log(moment('2023-02-24').toDate())
//格式化日期对象
// console.log(moment(new Date()).format('YYYY-MM-DD'));
//记账本的列表
router.get('/account', function(req, res, next) {
//获取所有的账单信息
// let accounts = db.get('accounts').value();
//读取集合信息
AccountModel.find().sort({time: -1}).exec((err, data) => {
if(err){
res.status(500).send('读取失败~~~');
return;
}
//响应成功的提示
res.render('list', {accounts: data, moment: moment});
})
});
//添加记录
router.get('/account/create', function(req, res, next) {
res.render('create');
});
//新增记录
router.post('/account', (req, res) => {
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if(err){
res.status(500).send('插入失败~~');
return
}
//成功提醒
res.render('success', {msg: '添加成功哦~~~', url: '/account'});
})
});
//删除记录
router.get('/account/:id', (req, res) => {
//获取 params 的 id 参数
let id = req.params.id;
//删除
AccountModel.deleteOne({_id: id}, (err, data) => {
if(err) {
res.status(500).send('删除失败~');
return;
}
//提醒
res.render('success', {msg: '删除成功~~~', url: '/account'});
})
});
module.exports = router;
- app.js中,导入接口的路由规则文件,并加上路由路径
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/web/index');
//导入 account 接口路由文件
const accountRouter = require('./routes/api/account');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/api', accountRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
获取账单接口(编写第一个接口)
目的:
- 编写第一个接口,分离HTTP服务响应中的ejs前端渲染模板部分,改为给前端返回json数据
在此前的HTTP服务基础上,加上api功能,步骤:
- 修改原有的http服务中的成功响应
- 将原有返回的渲染对象
res.render('list', {accounts: data, moment: moment});
- 修改为返回json响应内容对象
res.json({code: '0000',msg: '读取成功',data: data});
即json响应内容对象设置3个属性res.json({响应编号,响应的信息字符串,响应的数据});
- 将原有返回的渲染对象
- 修改原有的http服务中的失败响应
- 类似修改成功响应的操作
- 但不用设置返回的状态码(不再需要设置
res.status(500)
,因为一般接口服务返回的状态信息,已经标识在返回的code中,没有必要再通过响应状态码去设置响应状态,前端处理起来麻烦);通常,接口服务在响应结果时,就直接返回JSON,在JSON.code中去设置对应的编号即可
- 注意:
- 响应的编号,有两种写法,一种是20000(即200状态码*100);一种是0000或者000000(银联、微信项目接口的格式,0表示成功,非0表示失败)
- 修改原有的http服务中的成功响应
API接口响应与HTTP服务响应相比:
- HTTP服务响应:路由规则中,通过导入的模型对象获取数据,再通过
res.render()
交给ejs模板,生成html再返回浏览器 - API接口响应:路由规则中,通过导入的模型对象获取数据,通过
res.json({响应编号,响应的信息字符串,响应的数据});
,只给客户端返回json对象,客户端再去处理json对象,作相应的数据渲染 - 优势:
- 接口返回的结果是JSON的,各端通过JSON对象,获取具体数据,再根据各端的前端框架/模板去渲染
- HTTP服务响应:路由规则中,通过导入的模型对象获取数据,再通过
在此前的HTTP服务基础上,加上 获取账单接口 api功能,项目文件夹\routes\api\account.js
var express = require('express');
var router = express.Router();
//导入 moment
const moment = require('moment');
const AccountModel = require('../../models/AccountModel');
//测试
// console.log(moment('2023-02-24').toDate())
//格式化日期对象
// console.log(moment(new Date()).format('YYYY-MM-DD'));
//记账本的列表
router.get('/account', function(req, res, next) {
//获取所有的账单信息
// let accounts = db.get('accounts').value();
//读取集合信息
AccountModel.find().sort({time: -1}).exec((err, data) => {
if(err){
// 原有返回的渲染对象
// res.status(500).send('读取失败~~~');
// api接口化,改为返回json响应内容对象
res.json({
code: '1001',
msg: '读取失败~~',
data: null
})
return;
}
// 原有返回的渲染对象 响应成功的提示
// res.render('list', {accounts: data, moment: moment});
// api接口化,改为返回json响应内容对象 响应成功的提示
res.json({
//响应编号
code: '0000',
//响应的信息
msg: '读取成功',
//响应的数据
data: data
});
})
});
//添加记录
router.get('/account/create', function(req, res, next) {
res.render('create');
});
//新增记录
router.post('/account', (req, res) => {
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if(err){
res.status(500).send('插入失败~~');
return
}
//成功提醒
res.render('success', {msg: '添加成功哦~~~', url: '/account'});
})
});
//删除记录
router.get('/account/:id', (req, res) => {
//获取 params 的 id 参数
let id = req.params.id;
//删除
AccountModel.deleteOne({_id: id}, (err, data) => {
if(err) {
res.status(500).send('删除失败~');
return;
}
//提醒
res.render('success', {msg: '删除成功~~~', url: '/account'});
})
});
module.exports = router;
添加账单接口
步骤:
- 将原有的http服务中的 添加记录 路由规则删除
- 原有的http服务中的 添加记录 路由规则,是为了响应html页面的,在api接口响应中不需要(api接口不返回html内容,只返回JSON数据,删掉添加记录部分路由规则,不返回html形式(即res.render()的数据))
- 将原有的http服务中的 新增记录 中的 插入数据库 规则
- 修改为返回json响应内容对象
- 通过接口测试工具测试 新增数据 接口
- 表单形式的请求体数据,即 Querystring查询字符串 内容形式
- 改造前是通过表单req.body存入数据;
- 改造后,通过json设置,设置方式参考模型文件
- 要求app.js中的中间件设置,包括Querystring查询字符串
app.use(express.urlencoded({ extended: false }));
和JSON形式app.use(express.json());
- 补充:对请求体数据作表单验证/细致验证/逐项属性验证
- 参考模型文件对数据的要求,展开表单验证/细致验证/逐项属性验证
- 一旦检测到数据与要求不符,可以返回相应的状态码给客户端
- 将原有的http服务中的 添加记录 路由规则删除
在此前的HTTP服务基础上,加上 添加账单接口 api功能,项目文件夹\routes\api\account.js
var express = require('express');
var router = express.Router();
//导入 moment
const moment = require('moment');
const AccountModel = require('../../models/AccountModel');
//测试
// console.log(moment('2023-02-24').toDate())
//格式化日期对象
// console.log(moment(new Date()).format('YYYY-MM-DD'));
//记账本的列表
router.get('/account', function(req, res, next) {
//获取所有的账单信息
// let accounts = db.get('accounts').value();
//读取集合信息
AccountModel.find().sort({time: -1}).exec((err, data) => {
if(err){
// 原有返回的渲染对象
// res.status(500).send('读取失败~~~');
// api接口化,改为返回json响应内容对象
res.json({
code: '1001',
msg: '读取失败~~',
data: null
})
return;
}
// 原有返回的渲染对象 响应成功的提示
// res.render('list', {accounts: data, moment: moment});
// api接口化,改为返回json响应内容对象 响应成功的提示
res.json({
//响应编号
code: '0000',
//响应的信息
msg: '读取成功',
//响应的数据
data: data
});
})
});
// 删掉添加记录部分路由规则,不返回html形式(即res.render()的数据)
// router.get('/account/create', function(req, res, next) {
// res.render('create');
// });
//新增记录
router.post('/account', (req, res) => {
//表单验证
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if(err){
// 改为返回json
// res.status(500).send('插入失败~~');
res.json({
code: '1002',
msg: '创建失败~~',
data: null
})
return
}
//成功提醒
// res.render('success', {msg: '添加成功哦~~~', url: '/account'});
// 改为返回json
res.json({
code: '0000',
msg: '创建成功',
data: data
})
})
});
//删除记录
router.get('/account/:id', (req, res) => {
//获取 params 的 id 参数
let id = req.params.id;
//删除
AccountModel.deleteOne({_id: id}, (err, data) => {
if(err) {
res.status(500).send('删除失败~');
return;
}
//提醒
res.render('success', {msg: '删除成功~~~', url: '/account'});
})
});
module.exports = router;
- 借助GPT,对账单接口,补充完整 对接口的请求体数据作表单验证/细致验证/逐项属性验证
prompt:
请参照以下数据库的模型文件代码,以及数据库的新增接口代码,补充表单验证的逻辑到数据库新增接口的代码中
要求:
1、当缺少标题时,返回code错误代码1003,报错msg信息为缺少标题
2、当类型并非数字类型时,返回code错误代码1004,报错msg信息为请输入数字
3、当金额并非数字类型时,返回code错误代码1005,报错msg信息为请输入金额
数据库的模型文件代码如下:
//设置集合中文档的属性以及属性值的类型
let AccountSchema = new mongoose.Schema({
//标题
title: {
type: String,
required: true
},
//时间,类型是日期对象
time: Date,
//类型
type: {
// 数字类型
type: Number,
// 默认是支出
default: -1
},
//金额
account: {
type: Number,
required: true
},
//备注
remarks: {
type: String
}
})
数据库的新增接口代码如下:
//新增记录
router.post('/account', (req, res) => {
//表单验证
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if (err) {
res.json({
code: '1002',
msg: '创建失败~~',
data: null
})
return
}
//成功提醒
res.json({
code: '0000',
msg: '创建成功',
data: data
})
})
});
- GPT的回答如下
// 新增记录
router.post('/account', (req, res) => {
// 表单验证
// 检查标题是否存在
if (!req.body.title) {
res.json({
code: '1003',
msg: '缺少标题',
data: null
})
return
}
// 检查类型是否为数字类型
if (isNaN(req.body.type)) {
res.json({
code: '1004',
msg: '请输入数字',
data: null
})
return
}
// 检查金额是否为数字类型
if (isNaN(req.body.account)) {
res.json({
code: '1005',
msg: '请输入金额',
data: null
})
return
}
// 插入数据库
AccountModel.create({
...req.body,
// 修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if (err) {
res.json({
code: '1002',
msg: '创建失败~~',
data: null
})
return
}
// 成功提醒
res.json({
code: '0000',
msg: '创建成功',
data: data
})
})
})
在表单验证部分,我们添加了对标题、类型和金额的检查。如果标题不存在,类型或金额不是数字类型,则返回相应的错误代码和错误信息。否则,继续执行数据库插入操作。
//新增记录
router.post('/account', (req, res) => {
//表单验证
const { title, type, account } = req.body;
if (!title) {
return res.json({
code: '1003',
msg: '缺少标题',
data: null
});
}
if (typeof type !== 'number') {
return res.json({
code: '1004',
msg: '请输入数字',
data: null
});
}
if (typeof account !== 'number') {
return res.json({
code: '1005',
msg: '请输入金额',
data: null
});
}
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if (err) {
res.json({
code: '1002',
msg: '创建失败~~',
data: null
});
return;
}
//成功提醒
res.json({
code: '0000',
msg: '创建成功',
data: data
});
});
});
修改和新增的逻辑
标题验证:添加了 if (!title) 检查 title 是否存在,若不存在返回错误代码 1003。
类型验证:添加了 if (typeof type !== 'number') 检查 type 是否为数字类型,若不是返回错误代码 1004。
金额验证:添加了 if (typeof account !== 'number') 检查 account 是否为数字类型,若不是返回错误代码 1005。
以下是完整的数据库新增接口代码,包含所需的表单验证逻辑:
//新增记录
router.post('/account', (req, res) => {
//表单验证
if (!req.body.title) {
return res.json({
code: '1003',
msg: '缺少标题',
data: null
})
}
if (isNaN(Number(req.body.type))) {
return res.json({
code: '1004',
msg: '请输入数字',
data: null
})
}
if (isNaN(Number(req.body.account))) {
return res.json({
code: '1005',
msg: '请输入金额',
data: null
})
}
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if (err) {
res.json({
code: '1002',
msg: '创建失败~~',
data: null
})
return
}
//成功提醒
res.json({
code: '0000',
msg: '创建成功',
data: data
})
})
});
你认为哪一个更好呢?
删除账单接口
步骤:
- 将原有的http服务中的 删除记录 路由规则,由get方法改为delete
- 修改为返回json响应内容对象,由于是删除,返回的json内容对象中,data可以给NaN,也可以给空对象{}
- 将原有的http服务中的 删除记录 路由规则,由get方法改为delete
在此前的HTTP服务基础上,加上 删除账单接口 api功能,项目文件夹\routes\api\account.js
var express = require('express');
var router = express.Router();
//导入 moment
const moment = require('moment');
const AccountModel = require('../../models/AccountModel');
//测试
// console.log(moment('2023-02-24').toDate())
//格式化日期对象
// console.log(moment(new Date()).format('YYYY-MM-DD'));
//记账本的列表
router.get('/account', function(req, res, next) {
//获取所有的账单信息
// let accounts = db.get('accounts').value();
//读取集合信息
AccountModel.find().sort({time: -1}).exec((err, data) => {
if(err){
// 原有返回的渲染对象
// res.status(500).send('读取失败~~~');
// api接口化,改为返回json响应内容对象
res.json({
code: '1001',
msg: '读取失败~~',
data: null
})
return;
}
// 原有返回的渲染对象 响应成功的提示
// res.render('list', {accounts: data, moment: moment});
// api接口化,改为返回json响应内容对象 响应成功的提示
res.json({
//响应编号
code: '0000',
//响应的信息
msg: '读取成功',
//响应的数据
data: data
});
})
});
// 删掉添加记录部分路由规则,不返回html形式(即res.render()的数据)
// router.get('/account/create', function(req, res, next) {
// res.render('create');
// });
//新增记录
router.post('/account', (req, res) => {
//表单验证
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if(err){
// 改为返回json
// res.status(500).send('插入失败~~');
res.json({
code: '1002',
msg: '创建失败~~',
data: null
})
return
}
//成功提醒
// res.render('success', {msg: '添加成功哦~~~', url: '/account'});
// 改为返回json
res.json({
code: '0000',
msg: '创建成功',
data: data
})
})
});
//删除记录
router.get('/account/:id', (req, res) => {
//获取 params 的 id 参数
let id = req.params.id;
//删除
AccountModel.deleteOne({_id: id}, (err, data) => {
if(err) {
// res.status(500).send('删除失败~');
res.json({
code: '1003',
msg: '删除账单失败',
data: null
})
return;
}
//提醒
// res.render('success', {msg: '删除成功~~~', url: '/account'});
res.json({
code: '0000',
msg: '删除成功',
data: {}
})
})
});
module.exports = router;
获取单个账单接口
步骤:
- 在此前的路由文件中没有此路由规则,需要重新写
在此前的HTTP服务基础上,加上 获取单个账单接口 api功能,项目文件夹\routes\api\account.js
var express = require('express');
var router = express.Router();
//导入 moment
const moment = require('moment');
const AccountModel = require('../../models/AccountModel');
//测试
// console.log(moment('2023-02-24').toDate())
//格式化日期对象
// console.log(moment(new Date()).format('YYYY-MM-DD'));
//记账本的列表
router.get('/account', function(req, res, next) {
//获取所有的账单信息
// let accounts = db.get('accounts').value();
//读取集合信息
AccountModel.find().sort({time: -1}).exec((err, data) => {
if(err){
// 原有返回的渲染对象
// res.status(500).send('读取失败~~~');
// api接口化,改为返回json响应内容对象
res.json({
code: '1001',
msg: '读取失败~~',
data: null
})
return;
}
// 原有返回的渲染对象 响应成功的提示
// res.render('list', {accounts: data, moment: moment});
// api接口化,改为返回json响应内容对象 响应成功的提示
res.json({
//响应编号
code: '0000',
//响应的信息
msg: '读取成功',
//响应的数据
data: data
});
})
});
// 删掉添加记录部分路由规则,不返回html形式(即res.render()的数据)
// router.get('/account/create', function(req, res, next) {
// res.render('create');
// });
//新增记录
router.post('/account', (req, res) => {
//表单验证
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if(err){
// 改为返回json
// res.status(500).send('插入失败~~');
res.json({
code: '1002',
msg: '创建失败~~',
data: null
})
return
}
//成功提醒
// res.render('success', {msg: '添加成功哦~~~', url: '/account'});
// 改为返回json
res.json({
code: '0000',
msg: '创建成功',
data: data
})
})
});
//删除记录
router.get('/account/:id', (req, res) => {
//获取 params 的 id 参数
let id = req.params.id;
//删除
AccountModel.deleteOne({_id: id}, (err, data) => {
if(err) {
// res.status(500).send('删除失败~');
res.json({
code: '1003',
msg: '删除账单失败',
data: null
})
return;
}
//提醒
// res.render('success', {msg: '删除成功~~~', url: '/account'});
res.json({
code: '0000',
msg: '删除成功',
data: {}
})
})
});
//获取单个账单信息
router.get('/account/:id', (req, res) => {
//获取 id 参数
let { id } = req.params;
//查询数据库
AccountModel.findById(id, (err, data) => {
if (err) {
return res.json({
code: '1004',
msg: '读取失败~~',
data: null
})
}
//成功响应
res.json({
code: '0000',
msg: '读取成功',
data: data
})
})
});
module.exports = router;
- 借助GPT,写 获取单个账单接口 api功能
prompt:
请参照以下数据库的模型文件代码,以及数据库的新增接口、删除接口代码,补充一个“获取单个账单信息”的接口的代码,到路由规则中
要求:
1、按照RESTful API 风格定义,路径是/account/:id
2、当响应成功时,返回code代码0000,返回msg信息为读取成功
3、当响应失败时,返回code代码1004,返回msg信息为读取失败~~
数据库的模型文件代码如下:
//设置集合中文档的属性以及属性值的类型
let AccountSchema = new mongoose.Schema({
//标题
title: {
type: String,
required: true
},
//时间,类型是日期对象
time: Date,
//类型
type: {
// 数字类型
type: Number,
// 默认是支出
default: -1
},
//金额
account: {
type: Number,
required: true
},
//备注
remarks: {
type: String
}
})
数据库的新增接口、删除接口代码如下:
//新增记录
router.post('/account', (req, res) => {
//表单验证
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if (err) {
res.json({
code: '1002',
msg: '创建失败~~',
data: null
})
return
}
//成功提醒
res.json({
code: '0000',
msg: '创建成功',
data: data
})
})
});
//删除记录
router.delete('/account/:id', (req, res) => {
//获取 params 的 id 参数
let id = req.params.id;
//删除
AccountModel.deleteOne({ _id: id }, (err, data) => {
if (err) {
res.json({
code: '1003',
msg: '删除账单失败',
data: null
})
return;
}
//提醒
res.json({
code: '0000',
msg: '删除成功',
data: {}
})
})
});
- GPT的回答如下
// 获取单个账单信息
router.get('/account/:id', (req, res) => {
// 获取 params 中的 id 参数
let id = req.params.id;
// 查询数据库
AccountModel.findById(id, (err, data) => {
if (err) {
res.json({
code: '1004',
msg: '读取失败~~',
data: null
})
return
}
// 成功响应
res.json({
code: '0000',
msg: '读取成功',
data: data
})
})
})
我们按照 RESTful API 风格定义了获取单个账单信息的路由路径为 /account/:id,其中 :id 是账单的 ID。
在处理函数中,我们获取了 params 中的 id 参数,并使用 findById 方法查询数据库。如果查询成功,我们会返回 code 为 0000,msg 为 读取成功 的 JSON 响应,其中包含账单数据。如果查询失败,我们会返回 code 为 1004,msg 为 读取失败~~ 的 JSON 响应。
以下是补充的获取单个账单信息接口的代码,遵循 RESTful 风格:
//获取单个账单信息
router.get('/account/:id', (req, res) => {
//获取 params 的 id 参数
let id = req.params.id;
//查询单个账单信息
AccountModel.findById(id, (err, data) => {
if (err || !data) {
res.json({
code: '1004',
msg: '读取失败~~',
data: null
});
return;
}
//成功提醒
res.json({
code: '0000',
msg: '读取成功',
data: data
});
});
});
说明
路径定义:/account/:id 采用 RESTful 风格,使用 :id 作为路由参数表示账单的 ID。
使用 findById 查询:AccountModel.findById(id) 查询指定 ID 的账单信息。
成功响应:如果查询成功,返回 code: '0000',msg: '读取成功' 和数据 data。
失败处理:如果查询失败或没有找到记录,返回 code: '1004',msg: '读取失败~~',data: null。
此接口确保代码一致性和可读性,符合 RESTful API 的设计规范。
我将基于提供的代码示例直接创建获取单个账单信息的接口代码。根据要求,接口需要:
使用GET方法
路径为/account/:id
成功响应:code: '0000', msg: '读取成功'
失败响应:code: '1004', msg: '读取失败~~'
使用Mongoose的findById方法查询
保持与现有代码一致的风格
以下是获取单个账单信息的接口代码:
// 获取单个账单信息
router.get('/account/:id', (req, res) => {
// 获取params的id参数
let id = req.params.id;
// 查询数据库
AccountModel.findById(id, (err, data) => {
if (err || !data) {
res.json({
code: '1004',
msg: '读取失败~~',
data: null
})
return;
}
// 成功响应
res.json({
code: '0000',
msg: '读取成功',
data: data
})
})
});
该代码实现了:
GET方法获取单个账单
使用findById查询MongoDB
处理查询错误和未找到的情况
返回符合要求的响应格式
保持与现有新增和删除接口一致的代码风格
更新账单接口
步骤:
- 在此前的路由文件中没有此路由规则,需要重新写
- 使用patch方法,作一个局部更新
- 同时使用RESTful API风格
- 通过mangoDB的updateOne方法更新
AccountModel.updateOne(第一个参数是条件即id,第二个是更新的内容req.body发送时放在请求体中,回调函数)
在此前的HTTP服务基础上,加上 更新单个账单信息 api功能,项目文件夹\routes\api\account.js
var express = require('express');
var router = express.Router();
//导入 moment
const moment = require('moment');
const AccountModel = require('../../models/AccountModel');
//测试
// console.log(moment('2023-02-24').toDate())
//格式化日期对象
// console.log(moment(new Date()).format('YYYY-MM-DD'));
//记账本的列表
router.get('/account', function(req, res, next) {
//获取所有的账单信息
// let accounts = db.get('accounts').value();
//读取集合信息
AccountModel.find().sort({time: -1}).exec((err, data) => {
if(err){
// 原有返回的渲染对象
// res.status(500).send('读取失败~~~');
// api接口化,改为返回json响应内容对象
res.json({
code: '1001',
msg: '读取失败~~',
data: null
})
return;
}
// 原有返回的渲染对象 响应成功的提示
// res.render('list', {accounts: data, moment: moment});
// api接口化,改为返回json响应内容对象 响应成功的提示
res.json({
//响应编号
code: '0000',
//响应的信息
msg: '读取成功',
//响应的数据
data: data
});
})
});
// 删掉添加记录部分路由规则,不返回html形式(即res.render()的数据)
// router.get('/account/create', function(req, res, next) {
// res.render('create');
// });
//新增记录
router.post('/account', (req, res) => {
//表单验证
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if(err){
// 改为返回json
// res.status(500).send('插入失败~~');
res.json({
code: '1002',
msg: '创建失败~~',
data: null
})
return
}
//成功提醒
// res.render('success', {msg: '添加成功哦~~~', url: '/account'});
// 改为返回json
res.json({
code: '0000',
msg: '创建成功',
data: data
})
})
});
//删除记录
router.get('/account/:id', (req, res) => {
//获取 params 的 id 参数
let id = req.params.id;
//删除
AccountModel.deleteOne({_id: id}, (err, data) => {
if(err) {
// res.status(500).send('删除失败~');
res.json({
code: '1003',
msg: '删除账单失败',
data: null
})
return;
}
//提醒
// res.render('success', {msg: '删除成功~~~', url: '/account'});
res.json({
code: '0000',
msg: '删除成功',
data: {}
})
})
});
//获取单个账单信息
router.get('/account/:id', (req, res) => {
//获取 id 参数
let { id } = req.params;
//查询数据库
AccountModel.findById(id, (err, data) => {
if (err) {
return res.json({
code: '1004',
msg: '读取失败~~',
data: null
})
}
//成功响应
res.json({
code: '0000',
msg: '读取成功',
data: data
})
})
});
//更新单个账单信息
router.patch('/account/:id', (req, res) => {
//获取 id 参数值
let { id } = req.params;
//更新数据库
AccountModel.updateOne({ _id: id }, req.body, (err, data) => {
if (err) {
return res.json({
code: '1005',
msg: '更新失败~~',
data: null
})
}
//再次查询数据库 获取单条数据
AccountModel.findById(id, (err, data) => {
if (err) {
return res.json({
code: '1004',
msg: '读取失败~~',
data: null
})
}
//成功响应
res.json({
code: '0000',
msg: '更新成功',
data: data
})
})
});
});
module.exports = router;