如何构建一个简单的Node.js REST API

这篇文章中,我们会使用Express、Sequelize和MySQL构建一个Node.js Rest API。这里我们将使用Sequelize来与MySQL实例交互。

所需的应用程序

  • Dockerhttps://www.docker.com/products/docker-desktop),是一套平台即服务产品,其使用系统级虚拟化,以称为容器的软件包来交付软件。容器之间彼此隔离,各自将它们的软件、库和配置文件打包在一起;它们可以通过明确定义的通道来通信。
  • Node.jshttps://nodejs.org/en/),是基于Chrome的JavaScript运行时构建的平台,可轻松构建快速且可扩展的网络应用程序。Node.js是一个开源、跨平台的运行时环境,用于开发服务端和网络应用程序。
  • ExpressJShttps://expressjs.com/),是node.js上最受欢迎的Web框架之一。它建立在node.js的HTTP模块之上,并增加了对路由、中间件、视图系统等特性的支持。它非常简单,体积轻巧,不像其他那些想要无所不包的框架那样臃肿,也就不会牺牲开发人员手中的灵活性,让他们可以选择自己的设计。
  • Sequelizehttps://sequelize.org/),是基于promise,支持Postgres、MySQL、MariaDB、SQLite和微软SQL Server的Node.js ORM。它具有可靠的事务支持、关系、急切和延迟加载及读取复制等特性。
  • CORShttps://www.npmjs.com/package/cors),是用来提供Connect/Express中间件的node.js包,可使用各种选项来启用CORS。
  • body-parserhttps://github.com/expressjs/body-parser),在处理程序之前在一个中间件中解析传入的请求主体,它在req.body属性下可用。
  • Postmanhttps://www.getpostman.com/),是一个API(应用程序编程接口)开发工具,可帮助构建、测试和修改API。它具有发出各种HTTP请求(GET、POST、PUT和PATCH等)的能力。

Node.js Rest CRUD API概述

我们准备构建的Rest API可以按标题来创建、检索、更新、删除和查找帖子(post)。

首先我们做一个Express Web服务器。然后我们为MySQL数据库添加配置,使用Sequelize为Post创建一个模型,编写控制器。接下来,我们定义用来处理所有CRUD操作(包括自定义查找)的路由。

下表概述了将要导出的Rest API

下图是我们的项目结构:

现在开始创建Node.js应用

首先,我们创建一个文件夹:

$ mkdir node_rest_api_with_mysql
$ cd node_rest_api_with_mysql

接下来,我们使用package.json文件初始化Node.js应用:

npm init
name: (nodejs-express-sequelize-mysql) 
version: (1.0.0) 
description: Node.js Rest Apis with Express, Sequelize & MySQL.
entry point: (index.js) server.js
test command: 
git repository: 
keywords: nodejs, express, sequelize, mysql, rest, api, docker
author: Christos Ploutarchou
license: (ISC)
Is this ok? (yes) yes

如果你的PC上已经安装了MySQL,则可以忽略以下步骤

接下来,需要为mysql和phpMyAdmin安装docker。

  1. 安装Docker(在此处了解有关Docker安装的更多信息「https://docs.docker.com/install/」)
  2. 进入项目根目录
  3. up compose
docker-compose up -d
  • 访问phpmyadmin
your_ip:8183
Server: mysql
Username: root/root
Password: root/pass
  • 在终端上访问mysql
docker exec -it mysql_container_name mysql -u root -p

Docker phpmyadmin ENV

我们还要在项目上安装必要的模块:express、sequelize、mysql2和body-parser。

运行命令:

npm install express body-parser cors  sequelize mysql2 --save

安装完成后,package.json文件应如下所示:

{
  "name": "node_rest_api_with_mysql",
  "version": "1.0.0",
  "description": "Node.js Rest Api with Express, Sequelize, MySQL & phpMyAdmin .",
  "main": "server.js",
  "scripts": {
    "start": "nodemon server.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/cploutarchou/node_rest_api_with_mysql.git"
  },
  "keywords": [
    "node",
    "rest-api",
    "tutorial",
    "mysql",
    "phpMyAdmin",
    "docker",
    "node.js",
    "sequilize"
  ],
  "author": "Christos Ploutarchou",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/cploutarchou/node_rest_api_with_mysql/issues"
  },
  "homepage": "https://github.com/cploutarchou/node_rest_api_with_mysql#readme",
  "dependencies": {
    "body-parser": "^1.19.0",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "mysql2": "^2.1.0",
    "sequelize": "^5.21.5"
  },
  "devDependencies": {
    "nodemon": "^2.0.2"
  }
}

设置Express Web服务器

在我们的根目录中需要创建一个新的server.js文件:

const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
const server = express();
const db = require("./models");
const corsSettings = {
  originL: "http://localhost:8081"
};
const api = require("./routes/index");
server.use(cors(corsSettings));
// Parse request of content-type - application/json
server.use(bodyParser.json());
// parse requests of content-type -application/x-www-form-urlencoded
server.use(bodyParser.urlencoded({ extended: true }));
create a simple route
server.get("/", (_req, res) => {
   res.json({ message: "Welcome to node.js rest api application. Created for learning purposes by Christos Ploutarchou" });
});
// set listening ports for request
const port = process.env.PORT || 8080;
server.listen(port, () => {
  console.log("Server running on port : " + port );
});

我们在这里做的事情是:

  • 导入express、body-parser和cors模块:
    • Express用于构建Rest API。
    • body-parser帮助解析请求并创建req.body对象。
    • cors提供了Express中间件,以多种选项启用CORS。
  • 创建一个Express应用,然后使用app.use()方法添加body-parser和cors中间件。请注意,我们设置了原点:http://localhost:8081
  • 定义一个易于测试的GET路由。
  • 在端口8080上侦听传入请求。

现在运行以下命令来运行应用:

node server.js。

在浏览器中打开URL http://localhost:8080/,你将看到:

正确,第一步已经完成。在下一部分中我们将动用Sequelize。

配置MySQL数据库和Sequelize

在根文件夹中,我们创建一个单独的config文件夹,用来使用db.config.js文件进行配置,如下所示:

注意:如果你不使用docker compose项目,则需要使用本地环境凭据和信息来更新数据库信息。

module.exports = {
  HOST: "localhost",
  USER: "root",
  PASSWORD: "pass",
  DB: "restapi",
  dialect: "mysql",
  pool: {
    max: 10,
    min: 0,
    acquire: 30000,
    idle: 50000
  }
};

前五个参数用于MySQL连接。 pool是可选的,它将用于Sequelize连接池配置:

  • max:池中的最大连接数
  • min:池中的最小连接数
  • idle:连接释放之前可以空闲的最长时间(以毫秒为单位)
  • acquire:在引发错误之前,该池将尝试获取连接的最长时间(以毫秒为单位)

有关更多信息,你可以访问Sequelize构造函数的API参考(https://sequelize.org/master/class/lib/sequelize.js~Sequelize.html#instance-constructor-constructor)。

初始化Sequelize

我们将在app/models文件夹中初始化Sequelize,下一步中这个文件夹里会包含模型。

现在使用以下代码创建app/models/index.js:

const dbConfig = require("../config/db.config");
const Sequelize = require("sequelize");
const database = new Sequelize(dbConfig.DB, dbConfig.USER, dbConfig.PASSWORD, {
  host: dbConfig.HOST,
  dialect: dbConfig.dialect,
  operatorsAliases: false,
  pool: {
    max: dbConfig.pool.max,
    min: dbConfig.pool.min,
    acquire: dbConfig.pool.acquire,
    idle: dbConfig.pool.idle
  }
});
const db = {};
db.Sequelize = Sequelize;
db.databaseConf = database;
db.posts = require("./Sequelize.model")(database, Sequelize);
module.exports = db;

不要忘记在server.js中调用sync()方法:

const db = require("./models");
db.databaseConf.sync();

之后,你的server.js文件应该如下所示:

const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
const server = express();
const db = require("./models");
const corsSettings = {
  originL: "http://localhost:8081"
};
const api = require("./routes/index");
server.use(cors(corsSettings));
// Parse request of content-type - application/json
server.use(bodyParser.json());
// parse requests of content-type -application/x-www-form-urlencoded
server.use(bodyParser.urlencoded({ extended: true }));
create a simple route
server.get("/", (_req, res) => {
   res.json({ message: "Welcome to node.js rest api application. Created for learning purposes by Christos Ploutarchou" });
});
// set listening ports for request
const port = process.env.PORT || 8080;
server.listen(port, () => {
  console.log("Server running on port : " + port );
});
db.databaseConf.sync();

定义Sequelize模型

在models文件夹中,创建Sequelize.model.js文件,如下所示:

module.exports = (database, Sequelize) => {
  return database.define("restTutorial", {
    title: {
      type: Sequelize.STRING
    },
    description: {
      type: Sequelize.TEXT
    },
    published: {
      type: Sequelize.BOOLEAN
    },
    publisher: {
      type: Sequelize.STRING
    }
  });
};

这个Sequelize模型表示MySQL数据库中的restTutorials表。以下列将自动生成:id、title(标题)、description(描述)、published(已发布)、createdAt、updatedAt。 初始化Sequelize之后我们不需要编写CRUD函数,Sequelize支持下列所有功能:

这些函数将用在我们的控制器上。

创建控制器

app/controllers文件夹中,我们使用以下CRUD函数创建Post.js:

  • create
  • findAll
  • findOne
  • update
  • delete
  • deleteAll
  • findAllPublished
  • findByPublisherName
const db = require('../models')
const postObj = db.posts
const Op = db.Sequelize.Op
// 创建并保存一个新帖子
exports.create = (request, result) => {
}
// 将帖子对象保存到数据库
postObj.create(post).then(data => {
}
// 获取所有帖子 (接收带条件的数据).
exports.getAllPosts = (request, result) => {
}
// 按ID获取帖子对象
exports.getPostByID = (request, result) => {
}
// 按id更新一个帖子对象
exports.updatePostByID = (request, result) => {
}
// 按ID删除帖子对象
exports.deletePostByID = (request, result) => {
}
// 从数据库删除所有帖子对象
exports.deleteAllPosts = (request, result) => {
}
// 获取所有已发布帖子
exports.getAllPublishedPosts = (request, result) => {
}
// 按发布者名称获取所有帖子
exports.getAllPostsByPublisherName = (request, result) => {
}
// 按标题获取所有已发布帖子
exports.getPostByTitle = (request, result) => {
}

现在我们来实现这些函数。

创建一个新的帖子对象

// 创建并保存新帖子
exports.create = (request, result) => {
  if (!request.body.title) {
    result.status(400).send({
      message: "Content cannot be empty"
    });
  }
  // 创建一个帖子对象
  const post = {
    title: request.body.title,
    description: request.body.description,
    published: request.body.published ? request.body.published : false,
    publisher: request.body.publisher ? request.body.publisher : false
  };
  // 将帖子对象保存到数据库
  postObj.create(post).then(data => {
    result.send(data);
  }).catch(err => {
    result.status(500).send({
      message: err.message || "Some error occurred while saving."
    });
  });
};

获取所有对象(按帖子标题)

// 按标题获取所有已发布帖子
exports.getPostByTitle = (request, result) => {
  const title = request.query.title;
  postObj.findAll({
    where: {
      publisher: { [Op.like]: <code data-enlighter-language="generic" class="EnlighterJSRAW">%${title}%</code> },
      published: true
    }
  }).then(data => {
    result.send(data);
  }).catch(err => {
    result.status(500).send({
      message: err.message || "Something going wrong. Unable to retrieve data!"
    });
  });
};

在这个函数上,我们使用request.query.title从Request中获取查询字符串,并将其视为findAll()方法的条件。

获取单个帖子对象(按帖子ID)

// 按ID获取帖子对象
exports.getPostByID = (request, result) => {
  const paramID = request.params.id;
  console.log(paramID);
  console.log(paramID);
  postObj.findAll({
    where: { id: paramID }
  }).then(data => {
    result.send(data);
  }).catch(err => {
    result.status(500).send({
      message: err.message || <code data-enlighter-language="generic" class="EnlighterJSRAW">Some error occurred while retrieving data with id : ${paramID}</code>
    });
  });
};

按id更新帖子对象

// 按id更新一个帖子对象
exports.updatePostByID = (request, result) => {
  const id = request.params.id;
  postObj.update(request.body, {
    where: { id: id }
  }).then(num => {
    if (num === 1) {
      result.send({
        message: "Post object successfully updated."
      });
    } else {
      result.send({
        message: <code data-enlighter-language="generic" class="EnlighterJSRAW">Cannot update Post object with id=${id}!</code>
      });
    }
  }).catch(err => {
    result.status(500).send({
      message: err.message || <code data-enlighter-language="generic" class="EnlighterJSRAW">Error while updating Post object with id=${id}!</code>
    });
  });
};

按ID删除帖子对象

// 按id删除帖子对象
exports.deletePostByID = (request, result) => {
  const id = request.params.id;
  postObj.destroy({
    where: { id: id }
  }).then(num => {
    if (num === 1) {
      result.send({
        message: "Post object successfully deleted."
      });
    } else {
      result.send({
        message: <code data-enlighter-language="generic" class="EnlighterJSRAW">Cannot delete Post object with id=${id}!</code>
      });
    }
  }).catch(err => {
    result.status(500).send({
      message: err.message || <code data-enlighter-language="generic" class="EnlighterJSRAW">Cannot delete Post object with id=${id}!</code>
    });
  });
};

从数据库中删除所有帖子对象

// 从数据库删除所有帖子对象
exports.deleteAllPosts = (request, result) => {
  postObj.destroy({
    where: {},
    truncate: false
  }).then(nums => {
    result.send({
      message: <code data-enlighter-language="generic" class="EnlighterJSRAW">${nums} Post objects was deleted successfully!</code>
    });
  }).catch(err => {
    result.status(500).send({
      message: err.message || "Cannot delete Post objects. Something going wrong}!"
    });
  });
};

获取所有已发布的帖子

// 获取所有已发布帖子
exports.getAllPublishedPosts = (request, result) => {
  postObj.findAll({
    where: { published: true }
  }).then(data => {
    result.send(data);
  }).catch(err => {
    result.status(500).send({
      message: err.message || "Something going wrong. Unable to retrieve data!"
    });
  });
};

从数据库获取所有已发布的帖子对象

exports.getAllPosts = (request, result) => {
  postObj.findAll()
    .then(data => {
      result.send(data);
    }).catch(err => {
      result.status(500).send({
        message: err.message || "Some error occurred while retrieving data."
      });
    });
};

按发布者名称获取所有帖子

// 按发布者名称获取所有帖子
exports.getAllPostsByPublisherName = (request, result) => {
  const name = request.params.name;
  const condition = name ? { publisher: { [Op.like]: <code data-enlighter-language="generic" class="EnlighterJSRAW">%${name}%</code> } } : null;
  postObj.findAll({ where: condition }).then(data => {
    result.send(data);
  }).catch(err => {
    result.status(500).send({
      message: err.message || "Something going wrong. Unable to retrieve data!"
    });
  });
};

定义路由

当客户端使用HTTP请求(GET、POST、PUT、DELETE)发送对一个端点的请求时,我们需要设置路由来确定服务器的响应方式。

现在我们在route/文件夹中创建一个index.js文件,其内容如下:

const post = require("../controllers/Post");
const express = require("express");
const router = express.Router();
// 创建新帖子
router.post("/api/posts/create", post.create);
// // 检索所有帖子
router.get("/api/posts/all", post.getAllPosts);
// 检索所有已发布帖子
router.get("/api/posts/published", post.getAllPublishedPosts);
// 按发布者名称检索所有已发布帖子
router.get("/api/posts/publisher", post.getAllPostsByPublisherName);
// 按标题检索所有帖子
router.get("/api/posts", post.getPostByTitle);
// 按id检索帖子
router.get("/api/posts/:id", post.getPostByID);
// // 按id更新帖子
router.put("/api/post/update/:id", post.updatePostByID);
// // 按id删除帖子
router.delete("/api/post/delete/:id", post.deletePostByID);
// 删除所有帖子
router.delete("/api/posts/deleteAll", post.deleteAllPosts);
module.exports = router;

你可以看到我们使用了…controllers/Post中的一个控制器。 我们还需要在server.js中包含路由(在app.listen()之前):

const api = require("./routes/index");
server.use("/", api);

更新之后,我们的server.js文件应该如下所示:

const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
const server = express();
const db = require("./models");
const corsSettings = {
  originL: "http://localhost:8081"
};
const api = require("./routes/index");
server.use(cors(corsSettings));
// Parse request of content-type - application/json
server.use(bodyParser.json());
// parse requests of content-type -application/x-www-form-urlencoded
server.use(bodyParser.urlencoded({ extended: true }));
server.use("/", api);
// set listening ports for request
const port = process.env.PORT || 80;
server.listen(port, () => {
  console.log(<code data-enlighter-language="generic" class="EnlighterJSRAW">Server running on port : ${port}</code>);
});
// 如果你要删除已有的表并重新同步数据库,请运行以下函数
// db.dropRestApiTable();
db.databaseConf.sync();

注意:在开发过程中,你可能需要删除现有的表并重新同步数据库。因此我们要在models/index.js上创建一个新函数以应用这个步骤。

在index.js上添加以下函数:

db.dropRestApiTable = () => {
  db.databaseConf.sync({ force: true }).then(() => {
    console.log("restTutorial table just dropped and db re-synced.");
  });
};

要删除现有表时,可以在server.js文件上调用该函数:

db.dropRestApiTable();

测试API

使用以下命令运行我们的Node.js应用程序:

node server.js
Server running on port : 80
Executing (default): CREATE TABLE IF NOT EXISTS restTutorials (id INTEGER NOT NULL auto_increment , title VARCHAR(255), description TEXT, published TINYINT(1), publisher VARCHAR(255), createdAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB;
Executing (default): SHOW INDEX FROM restTutorials

我们将使用Postman测试以上所有的API。

1. 使用/api/posts/create API创建一个新帖子

下面是我们的发帖请求示例:

{
    "title": "JS Tutorials : Part 1",
    "description": "Node.js Rest APIs with Express, Sequelize & MySQL Part 1",
    "published": true,
    "publisher": "Christos Ploutarchou"
}

当我们发送发帖请求(如果数据存储在数据库上)时,我们应该会收到STATUS: 200OK

注意:如果使用docker-compose运行MySQL,则可以使用以下凭据username: root | password: pass在localhost:8183上访问phpMyAdmin。

创建了一些新帖子后,你可以在phpMyAdmin上运行以下查询来检查MySQL表

select * from posts;

你的输出应该如下图所示:

2. 使用GET /api/posts/all API检索所有帖子

你应该获得如下图所示的反馈:

获取所有帖子

3. 使用GET /api/posts/:id API检索所有帖子

按ID获取帖子

4. 使用PUT /api/post/update/:id API更新帖子

按ID更新帖子

5. 使用GET /api/posts?title=tutorial API查找所有包含单词“tutorials”的帖子

按标题获取帖子

6. 使用GET /api/posts/publisher/?name=Christos API按发布者名称查找所有帖子

按发布者名称获取所有帖子

7. 使用GET /api/posts/published API查找所有已发布的帖子

获取所有已发布的帖子

8. 使用DELETE /api/posts/delete/:id API删除帖子

按帖子ID删除帖子

9. 使用DELETE /api/posts/deleteAll API删除所有帖子

删除所有帖子

你可以通过我的GitHub存储库下载项目的完整副本(https://github.com/cploutarchou/node_rest_api_with_mysql)。(如果你喜欢我的项目,请留下一颗星星)

原文链接:https://christosploutarchou.com/how-to-build-simple-node-js-rest-api

本文最初发布于Christos Ploutarchou博客,经原作者授权由InfoQ中文站翻译并分享。

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/W2NlDcVzU3YYqd0YFH6B
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券