Categories
程式開發

如何構建一個簡單的Node.js REST API


本文最初發佈於Christos Ploutarchou博客,經原作者授權由InfoQ中文站翻譯並分享。

這篇文章中,我們會使用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 REST API 1

下圖是我們的項目結構:

如何構建一個簡單的Node.js REST API 2

現在開始創建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

如何構建一個簡單的Node.js REST API 3

我們還要在項目上安裝必要的模塊: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/,你將看到:

如何構建一個簡單的Node.js REST API 4

正確,第一步已經完成。在下一部分中我們將動用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]: %${title}% },
      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 || Some error occurred while retrieving data with id : ${paramID}
    });
  });
};

按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: Cannot update Post object with id=${id}!
      });
    }
  }).catch(err => {
    result.status(500).send({
      message: err.message || Error while updating Post object with id=${id}!
    });
  });
};

按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: Cannot delete Post object with id=${id}!
      });
    }
  }).catch(err => {
    result.status(500).send({
      message: err.message || Cannot delete Post object with id=${id}!
    });
  });
};

從數據庫中刪除所有帖子對象

// 从数据库删除所有帖子对象
exports.deleteAllPosts = (request, result) => {
  postObj.destroy({
    where: {},
    truncate: false
  }).then(nums => {
    result.send({
      message: ${nums} Post objects was deleted successfully!
    });
  }).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]: %${name}% } } : 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(Server running on port : ${port});
});
// 如果你要删除已有的表并重新同步数据库,请运行以下函数
// 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;

你的輸出應該如下圖所示:

如何構建一個簡單的Node.js REST API 5

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

你應該獲得如下圖所示的反饋:

如何構建一個簡單的Node.js REST API 6

獲取所有帖子

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

如何構建一個簡單的Node.js REST API 7

按ID獲取帖子

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

如何構建一個簡單的Node.js REST API 8

按ID更新帖子

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

如何構建一個簡單的Node.js REST API 9

按標題獲取帖子

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

如何構建一個簡單的Node.js REST API 10

按發布者名稱獲取所有帖子

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

如何構建一個簡單的Node.js REST API 11

獲取所有已發布的帖子

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

如何構建一個簡單的Node.js REST API 12

按帖子ID刪除帖子

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

如何構建一個簡單的Node.js REST API 13

刪除所有帖子

你可以通過我的GitHub存儲庫下載項目的完整副本(https://github.com/cploutarchou/node_rest_api_with_mysql)。 (如果你喜歡我的項目,請留下一顆星星)

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