Node接口设计最佳实践

导语:之前做过一个小项目,其中后端用的node开发的接口服务,有些感觉挺好的方案,希望可以总结归纳一下,以免日后忘记,留个备份,记录一下。

# 目录

  • 简介
  • 规范
  • 工具
  • 案例

# 简介

# 定义

说到接口,大家应该都不陌生,对于程序员朋友来说是很常见的一种东西。API全称是Application Programming Interface,应用程序编程接口。

API是一些预先定义好的函数,提供应用程序和编程开发人员某种软件或硬件的能力,不需要访问源码或者理解其内部工作机制,根据API说明文档即可使用。

# 分类

API分为很多类型,常用的安装中间件划分有远程过程调用、标准查询语言、文件传输、信息交付等类型。

也有人分为系统级API和软件级API。

  • 系统级API

系统级API用来控制硬件,是操作系统和应用程序之间的接口,操作系统已经实现了很多功能,都被封装成了一个个的函数,想要使用哪个功能,就调用相关的函数。

比如Windows、Linux、MacOS、Unix等常见的操作系统大部分功能由C语言开发,所以API以C语言形式呈现,这些API成百上千,都有官方的说明文档以供查阅。

  • 软件级API

对于应用来说,经常用到一些功能,是由软件提供的API,包括编程语言API,自带的标准库,不需要再重复造轮子了,例如C的printf()scanf()fopen()

还要第三方库的API,比如OpenGLOpenCVopenssliconvCURL,js中也有很多的API,提供操作游览器窗口、网页文档等的功能。

另外还有组织机构、公司和个人提供的API,有的开源免费,有的闭源收费,比如天气获取接口、音乐接口、物流接口。

# 规范

# RESTful架构

REST是由HTTP协议(1.0版和1.1版)的主要设计者、Apache服务器软件的作者之一、Apache基金会的第一任主席Roy Thomas Fielding于2000年在他的博士论文中提出来的。

他在文中说到"我这篇文章的写作目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。"

Fielding将这种互联网软件的架构原则定名为Representational State Transfer,表现层状态转化,缩写是REST,符合这个REST原则的架构,就可以叫做RESTful架构。

  • 资源是网络中的一个具体信息,比如一个文本,图片,音乐,视频,服务等;
  • 表现层是把资源表现出来的形式,文本用txt格式表现,图片用jpg格式表现;
  • 状态转化是客户端和服务器互动过程中数据和状态的变化,GET、POST、PUT、DELETE;

# 设计误区

  • URL包含动词

资源是实体,应该用名词,动词放在HTTP中,比如:/cats/show/1,show是动词,正确应该是/cats/1,用get表示show;

再比如动词表示不了,把动词改为名词,/transfer改为/transition;

  • URL中包含版本号

比如/api/v1/article/api/v2/article/api/v3/article

不同的版本是同一种资源的不同表现形式,应该采用同一个URI才恰当。

可以放在请求头中,是一种推荐的做法,像Github就是不错的例子。

accept: text/html,application/xhtml+xml,application/xml;v=b3;
accept: application/json;version=1.0;
accept: application/json;version=2.0;
accept: application/json;version=3.0;
1
2
3
4

# URL设计

  • HTTP方法

GET:读取(Read),POST:新建(Create),PUT:更新(Update),PATCH:更新(Update);DELETE:删除(Delete);

Resource POST GET PUT DELETE
/customers Create a new customer Retrieve all customers Bulk update of customers Remove all customers
/customers/1 Error Retrieve the details for customer 1 Update the details of customer 1 if it exists Remove customer 1
/customers/1/orders Create a new order for customer 1 Retrieve all orders for customer 1 Bulk update of orders for customer 1 Remove all orders for customer 1
  • HTTP状态码

1xx:相关信息,2xx:操作成功,3xx:重定向,4xx:客户端错误,5xx:服务器错误;

  • 返回数据

尽量是json或者xml格式的数据,不要返回文本格式的,请求头的Accept可以设置成接受application/json的数据。

  • 返回状态

在返回的状态中,操作成功返回2xx,操作错误返回4xx比较好,这样有助于理清关系。

# 工具

这里我罗列了一些与API有关的工具,都是我平时用到的,比较简单方便调试API。

其中,我用的时间最长的就是postman,支持本地和远程访问,存储接口地址、参数,同步远程,导出导入接口文档,非常方便快捷,可以调试接口,很好的一款接口管理工具。

# 案例

下面,通过一个用户的简单增删查改来实现一些功能,其中有些方法可能没有封装优化什么的,请勿见怪。

这是一个商品表,通过/goods这个接口来进行资源的操作。

# 接口说明

  • /goods是接口地址;
  • GET:用来查询所有的商品;/goods?id=1,查询id是1的商品;
  • POST:用来创建商品;
  • PUT:用来更新商品;
  • DELETE:用来删除商品,/goods?id=1,删除id是1的商品;

# 生成项目

express --view=ejs mygoods
cd mygoods
npm i
npm start
1
2
3
4

# 配置数据库

在根目录下面创建一个名为model的文件夹,里面包含一个配置文件和一个调用方法文件。

// /model/config.js
const mysqlConfig = {
    host: 'localhost',
    port: '3306',
    user: 'test',
    password: 'test123456.',
    database: 'test'
};

module.exports = mysqlConfig;
1
2
3
4
5
6
7
8
9
10
// /model/db.js
const mysql = require('mysql');
const config = require('./config');

const db = mysql.createConnection(config);

db.connect(function (err) {
    if (err) {
        console.error(`error connecting: ${err.stack}`);
        return;
    }
    console.log(`Mysql is connected! 连接id: ${db.threadId}`);
});

module.exports = db;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 数据表内容

  • 数据表结构
CREATE TABLE `goods` (
  `id` int NOT NULL COMMENT 'id',
  `name` char(20) NOT NULL COMMENT '商品名称',
  `number` int DEFAULT NULL COMMENT '商品数量',
  `price` float DEFAULT NULL COMMENT '商品价格',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建日期',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新日期'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品表';
1
2
3
4
5
6
7
8
  • 表的索引
ALTER TABLE `goods`
  ADD PRIMARY KEY (`id`);
1
2
  • 使用表AUTO_INCREMENT
ALTER TABLE `goods`
  MODIFY `id` int NOT NULL AUTO_INCREMENT COMMENT 'id', AUTO_INCREMENT=5;
COMMIT;
1
2
3

# 写接口

routes文件夹下面增加一个goods.js的文件用来存放路由。

由于时间问题,我就不增加控制器了,直接在路由里面写方法。

  • 引入模块
// goods.js
const express = require('express');
const router = express.Router();
const db = require('../model/db');
1
2
3
4
  • 查询商品
router.get('/', (req, res) => {
    let result = {};
    let sql = 'SELECT * FROM `goods`';
    let params = req.query;
    if (params && params.id) {
        sql += ' WHERE id =' + params.id;
    }
    db.query(sql, function (error, results, fields) {
        if (error) {
            result = {
                code: 101,
                msg: 'get_fail',
                data: {
                    info: "查询失败!"
                }
            }
        };
        result = {
            code: 200,
            msg: 'get_succ',
            data: {
                info: "查询成功!",
                list: results
            }
        }
        return res.json(result);
    });
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • 新增商品
router.post('/', (req, res) => {
    let result = {};
    let params = req.body;
    let names = '',values = '';
    for (const key in params) {
        names += '`'+ key + '`,';
        if (key == 'name') {
            values += `"${params[key]}",`;
        } else {
            values += `${params[key]},`;
        }
    }
    names = names.slice(0, names.length-1);
    values = values.slice(0, values.length-1);
    db.query('SELECT id FROM `goods` WHERE name= "' + params.name + '"', function (error, results, fields) {
        if (error) {
            result = {
                code: 101,
                msg: 'get_fail',
                data: {
                    info: "查询失败!"
                }
            }
        };
        if (results && results.length) {
            result = {
                code: 200,
                msg: 'get_succ',
                data: {
                    info: "商品已存在!"
                }
            }
            return res.json(result);
        }
        db.query('INSERT INTO `goods`(' + names + ') VALUES (' + values + ')', function (error, results, fields) {
            if (error) {
                result = {
                    code: 101,
                    msg: 'save_fail',
                    data: {
                        info: "查询失败!"
                    }
                }
            };
            result = {
                code: 200,
                msg: 'save_succ',
                data: {
                    info: "保存成功!",
                    des: {
                        id: results.insertId
                    }
                }
            }
            return res.json(result);
        });
    });
    
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  • 更新商品
router.put('/', (req, res) => {
    let result = {};
    let params = req.body;
    if (!params.id) {
        return res.json({
            code: 101,
            msg: 'get_fail',
            data: {
                info: "id不能为空!"
            }
        })
    }
    db.query('SELECT id FROM `goods` WHERE `id` = ' + params.id, function (error, results, fields) {
        if (error) {
            result = {
                code: 101,
                msg: 'get_fail',
                data: {
                    info: "查询失败!"
                }
            }
        };
        if (results && results.length == 0) {
            result = {
                code: 200,
                msg: 'get_succ',
                data: {
                    info: "商品不存在!"
                }
            }
            return res.json(result);
        }
        db.query('UPDATE `goods` SET `number` = ' + params.number + ', `price` = ' + params.price + ' WHERE `id` = ' + params.id, function (error, results, fields) {
            if (error) {
                result = {
                    code: 101,
                    msg: 'save_fail',
                    data: {
                        info: "修改失败!"
                    }
                }
            };
            result = {
                code: 200,
                msg: 'save_succ',
                data: {
                    info: "修改成功!"
                }
            }
            return res.json(result);
        });
    });
    
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
  • 删除商品
router.delete('/', (req, res) => {
    let result = {};
    let params = req.query;
    if (!params.id) {
        return res.json({
            code: 101,
            msg: 'get_fail',
            data: {
                info: "id不能为空!"
            }
        })
    }
    db.query('SELECT id FROM `goods` WHERE `id` = ' + params.id, function (error, results, fields) {
        if (error) {
            result = {
                code: 101,
                msg: 'get_fail',
                data: {
                    info: "查询失败!"
                }
            }
        };
        if (results && results.length == 0) {
            result = {
                code: 200,
                msg: 'get_succ',
                data: {
                    info: "商品不存在!"
                }
            }
            return res.json(result);
        }
        db.query('DELETE FROM `goods` WHERE `id` = ' + params.id, function (error, results, fields) {
            if (error) {
                result = {
                    code: 101,
                    msg: 'get_fail',
                    data: {
                        info: "删除失败!"
                    }
                }
            };
            result = {
                code: 200,
                msg: 'get_succ',
                data: {
                    info: "删除成功!"
                }
            }
            return res.json(result);
        });
    });
    
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
  • 导出模块
module.exports = router;
1
  • app.js里面加入下面两行。
var goodsRouter = require('./routes/goods');
app.use('/goods', goodsRouter);
1
2

# 测试接口

打开postman,配置好环境,添加四个请求,开始测试了。

  • 查询所有商品

查询所有商品

  • 查询单个商品

查询单个商品

  • 创建商品

创建商品

  • 更新商品

更新商品

  • 删除商品

删除商品

# 写在最后

互联网开发者社区中,还要非常多的好的设计方案,我这只是我所了解到的一部分而已,有好的也可以联系我分享给我。

以上就是一个node接口设计最佳实践,可能由一些不足之处,还请见谅,可以邮箱发我提建议,谢谢!

分享至:

  • qq
  • qq空间
  • 微博
  • 豆瓣
  • 贴吧