基於node.js、mariaDB實現RESTful API
===
###### tags: `RESTful`
node.js是近幾年開始流行的後端語言,使用node.js可以取代傳統的php、apache組合,並且可以直接以javaScript處理後端邏輯與伺服器建置等工作,統一前後端程式撰寫語法。node.js與php不同之處為,php為阻塞式I/O,前一條命令執行完成才執行下一條命令,node.js為非阻塞式I/O,所有命令同時執行,並且使用回呼函數來確定命令已經執行完畢。
:::success
:bookmark: **目錄**
:::spoiler
[TOC]
:::
## 使用套件介紹
node.js背後擁有巨大的套件庫,我們可以利用node.js同捆的[npm套件管理器](https://hackmd.io/3G8cb72ZTs6ces_BLAKz6Q),下載數以萬計的javaScript套件,透過他人已經開發完成的套件,來縮短專案的撰寫時間,開發出更加成熟的專案。
### express
```
npm install express
```
使用express可以快速搭建功能完整的網站伺服器,使用express可以幫助我們替請求方法以及URLs定義路由。基於express我們可以快速搭建起功能完整的RESTful API。
[官方網站](https://expressjs.com/zh-tw/)
### body-parser
```
npm install body-parser
```
body-parser用於處理從客戶端傳輸而來的request,可以直接獲得POST、PUT中的body內容,解析json、urlencoded、text等多種檔案格式。
### mariadb
```
npm install mariadb
```
mariadb官方所提供的連線庫,可以處理所有與mariadb資料庫溝通的需求。
### sequelize
```
npm install sequelize@next
```
sequelize為[ORM框架](https://zh.wikipedia.org/wiki/%E5%AF%B9%E8%B1%A1%E5%85%B3%E7%B3%BB%E6%98%A0%E5%B0%84)(Object Relationship Model),我們可以使用物件導向的方式操作資料庫中的表,所有資料庫的表都會映射到sequelize的物件之中,可以便利地操作查詢。
在這個筆記中,我們使用的是BETA 5的版本(sequelize@next),因為sequelize從第五版開始支援mariadb。當配上上述的mariadb官方連線庫,即可建立ORM風格的程式。
sequelize操作的方法與屬性請參閱[官方手冊](https://)或是[這篇文章](https://segmentfault.com/a/1190000003987871#articleHeader24)。
## 資料庫與API設計
在開始建立起第一個RESTful API前,我們得先規劃資料庫以及API架構。
假設今天我們有一個留言板的需求,必須滿足留言板的新增、修改、移除與查閱等功能。且為了凸顯RESTful設計風格,在接下來的設計中我們將暫時忽略帳戶權限的功能,以完成API為優先要件。若能理解整體運作,再整合帳戶權限等功能進入程式,並不是太大問題。
### 資料表設計
```sql
CREATE TABLE `blog`.`borad` (
`id` INT(11) NOT NULL AUTO_INCREMENT ,
`title` VARCHAR(255) NOT NULL ,
`author` VARCHAR(255) NOT NULL ,
`content` VARCHAR(255) NOT NULL ,
`addTime` DATE NOT NULL ,
`modifyTime` DATE NOT NULL ,
PRIMARY KEY (`id`)
)ENGINE=InnoDB;
```
### HTML動詞設計
|名稱(API Name)| 路由(Route) |HTML動詞(Verb)| 描述(Description) |
| -------- | -------- | -------- | -------- |
|Create MSG| /api/borad | POST | 新增一則留言 |
|MSG List| /api/borad | GET | 獲取留言列表 |
|Get Single MSG| /api/borad/:id | GET | 獲取一則內容 |
|Edit MSG| /api/borad/:id | PUT | 編輯內容,更新時間 |
|Delete MSG| /api/borad/:id | DELETE | 刪除一筆留言 |
### 回傳結構
``` HTML
API : Create MSG
URL :
http://localhost/api/borad
METHOD :
POST
PARAMETERS :
title : String
author : String
content : String
RETURN :
STATUS : 200 OK
JSON :
{
"id" : xx
"title" : "xxxxx",
"author" : "xxxxx",
"content" : "xxxxx",
"addTime" : "yyyy-mm-ddThh:mm:ssZ",
"modifyTime" : "yyyy-mm-ddThh:mm:ssZ"
}
API : MSG List
URL :
http://localhost/api/borad
METHOD :
GET
RETURN :
STATUS : 200 OK
JSON :
[
{
"id" : xx
"title" : "xxxxx",
"author" : "xxxxx",
"addTime" : "yyyy-mm-ddThh:mm:ssZ",
"modifyTime" : "yyyy-mm-ddThh:mm:ssZ"
},
{
"id" : xx
"title" : "xxxxx",
"author" : "xxxxx",
"addTime" : "yyyy-mm-ddThh:mm:ssZ",
"modifyTime" : "yyyy-mm-ddThh:mm:ssZ"
},
......(省略)
]
API : Get Single MSG
URL :
http://localhost/api/borad/:id
METHOD :
GET
RETURN :
STATUS : 200 OK
JSON :
{
"id" : xx
"title" : "xxxxx",
"author" : "xxxxx",
"content" : "xxxxx",
"addTime" : "yyyy-mm-ddThh:mm:ssZ",
"modifyTime" : "yyyy-mm-ddThh:mm:ssZ"
}
API : Edit MSG
URL :
http://localhost/api/borad/:id
METHOD :
PATCH
PARAMETERS :
name : String
content : String
RETURN :
STATUS : 200 OK
JSON :
{
"id" : xx
"title" : "xxxxx",
"author" : "xxxxx",
"content" : "xxxxx",
"addTime" : "yyyy-mm-ddThh:mm:ssZ",
"modifyTime" : "yyyy-mm-ddThh:mm:ssZ"
}
API : Delete MSG
URL :
https://localhost/api/borad/:newsId
METHOD :
Delete
RETURN :
STATUS : 200 OK
```
### Model-Controller-Route
借艦於目前主流的開發架構MVC,我們在規劃API程式時,也可以將程式碼做一定的切割,但因為API重於伺服器回傳以及溝通,並沒有view的概念。於是我們將Route(路由)獨立出來,增加程式簡潔以及可維護性。
![](https://i.imgur.com/94xS59H.png)
1. 所有請求進入伺服器時都會經過Route所設定的規則。
2. Route會分析請求方法為何,並將程式分配到對應的Controller。
* express : 負責解析不同的請求方法以及路由設定。
3. Controller將頁面解析的結果經過判斷以及處理過傳送給Model。
* body-parser : 負責解析頁面傳來的資訊(POST or PUT)。
5. Model接收到Controller的動作後執行查詢。
* mariadb : 與資料庫進行連接。
* sequelize : 允許我們以物件導向的方式存取資料庫內容。
6. DB回傳對應資訊給予Model
7. Model將資訊回傳給Controller,包含查詢成功或失敗。
8. 將正確結果回傳給請求方,或者是錯誤失敗的狀態及訊息。
## 程式實作
在完成了程式規劃後,將會開始程式的實現,在接下來的說明中,將不會對基本的node.js語法進行解釋。需要具備基本的javaScript與node.js等知識,便能完成整個程式設計。
### 資料夾與檔案結構
```
REST-API
├─config
│ └─config.json
├─controllers
│ ├─borad.js
│ └─index.js
├─models
│ ├─borad.js
│ └─index.js
├─routes
│ ├─borad.js
│ └─index.js
└─index.js
```
### config/config.json
```json
{
"development": {
"username": "root",
"password": "",
"database": "blog",
"host": "127.0.0.1",
"port": 3306,
"dialect": "mariadb"
}
}
```
定義資料庫連線方法與帳號密碼,日後可以分成開發中的資料庫與實際上線的資料庫。
### models/borad.js
```javascript=1
module.exports = (sequelize, DataTypes) => {
const borad = sequelize.define('borad', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
author: {
type: DataTypes.STRING,
allowNull: false,
},
content: {
type: DataTypes.INTEGER,
allowNull: false,
},
addTime:{
type:DataTypes.DATE
},
modifyTime:{
type:DataTypes.DATE
}
},{
timestamps:false,
freezeTableName:true
});
return borad;
};
```
將實際資料表的結構映射進sequelize套件中,注意欄位的規則必須與資料庫相同,否則將會在日後執行查詢時發生錯誤。
在這個文件中,將會return整個設定好的物件。
sequelize操作的方法與屬性請參閱[官方手冊](https://)或是[這篇文章](https://segmentfault.com/a/1190000003987871#articleHeader24)。
### models/index.js
```javascript=1
//讀取檔案套件
const fs = require('fs');
//處理文件路徑套件
const path = require('path');
//資料庫連接套件
const Sequelize = require('sequelize');
//設定預設文件路徑,module.filename將會回傳該原始碼所在的文件夾
const basename = path.basename(module.filename);
//目前系統環境為開發中,若有設定環境變數,將可以讓程式自適應環境,切換所要連線的伺服器。
const env = /*process.env.NODE_ENV || */'development';
//設定檔載入
const config = require(`${__dirname}/../config/config.json`)[env];
const db = {};
//初始化資料庫連線
let sequelize = new Sequelize(
config.database, config.username, config.password, config
);
//將資料夾內的.js檔案依序實例,並註冊在同一物件之中。
fs.readdirSync(__dirname)
.filter((file) =>
(file.indexOf('.') !== 0) &&
(file !== basename) &&
(file.slice(-3) === '.js'))
.forEach((file) => {
const model = sequelize.import(path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach((modelName) => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;
```
### controllers/borad.js
```javascript=1
//載入相對應的model
const Borad = require('../models/index').borad;
module.exports = {
//創建
create(req, res) {
return Borad
.create({
title: req.body.title,
author: req.body.author,
content: req.body.content,
addTime: new Date(new Date(new Date).valueOf() + 8 * 60 * 60 * 1000),
modifyTime: new Date(new Date(new Date).valueOf() + 8 * 60 * 60 * 1000)
})
.then((borad) => res.status(200).send(borad))
.catch((error) => res.status(400).send(error));
},
//列表項
list(req, res) {
return Borad
.findAll({
attributes : ['id','title','author','addTime','modifyTime'],
order: [
['id', 'DESC']
],
})
.then((borad) => res.status(200).send(borad))
.catch((error) => res.status(400).send(error));
},
//取得某項
retrieve(req, res) {
return Borad
.findByPk(req.params.id, {
})
.then((borad) => {
if (!borad) {
return res.status(404).send({
message: '找不到這筆留言',
});
}
return res.status(200).send(borad);
})
.catch((error) => res.status(400).send(error));
},
//更新
update(req, res) {
return Borad
.findByPk(req.params.id, {
})
.then(borad => {
if (!borad) {
return res.status(404).send({
message: '找不到這筆留言',
});
}
return borad
.update({
title: req.body.title || borad.title,
author: req.body.author || borad.author,
content: req.body.content || borad.content,
modifyTime: new Date(new Date(new Date).valueOf() + 8 * 60 * 60 * 1000)
})
.then(() => res.status(200).send(borad))
.catch((error) => res.status(400).send(error));
})
.catch((error) => res.status(400).send(error));
},
//刪除
destroy(req, res) {
return Borad
.findByPk(req.params.id)
.then(borad => {
if (!borad) {
return res.status(400).send({
message: '找不到這筆留言',
});
}
return borad
.destroy()
.then(() => res.status(204).send())
.catch((error) => res.status(400).send(error));
})
.catch((error) => res.status(400).send(error));
}
};
```
在Controller中,定義每一個動作,對應我們所規劃的行為。這些對應行為的設定方法可以參考上面所提到的sequelize手冊。
### controllers/index.js
```javascript=1
//非同步讀取檔案套件
const fs = require('fs');
//處理文件路徑套件
const path = require('path');
const allControllers = {};
//設定預設文件路徑,module.filename將會回傳該文件夾中的預設文件index.js
const basename = path.basename(module.filename);
//依序讀取目錄中檔案,並且一一引入至同一物件中
fs.readdirSync(__dirname)
.filter((file) =>
(file.indexOf('.') !== 0) &&
(file !== basename) &&
(file.slice(-3) === '.js'))
.forEach((file) => {
const fileName = file.replace('.js','');
const controller = require('./'+fileName);
allControllers[fileName] = controller;
});
//判斷非同步是否完成,完成後放入物件中。
Object.keys(allControllers).forEach((controllerName) => {
if (allControllers[controllerName].associate) {
allControllers[controllerName].associate(allControllers);
}
});
const borad = require('./borad');
module.exports = allControllers;
```
### routes/borad.js
```javascript=1
var express = require('express');
var router = express.Router();
const boradController = require('../controllers/index').borad;
/* GET home page. */
router.get('/', function(req, res, next) {
boradController.list(req,res)
});
router.get('/:id', function(req, res, next) {
boradController.retrieve(req,res)
});
router.post('/', function(req, res, next) {
console.log(req.body);
boradController.create(req,res)
});
router.delete('/:id', function(req, res, next) {
boradController.destroy(req,res)
});
router.put('/:id', function(req, res, next) {
boradController.update(req,res)
});
module.exports = router;
```
根據所規劃的URL以及動詞,將請求導向到正確的controller。
### routes/index.js
```javascript=1
//非同步讀取檔案套件
const fs = require('fs');
//處理文件路徑套件
const path = require('path');
const allRoutes = {};
//設定預設文件路徑,module.filename將會回傳該文件夾中的預設文件index.js
const basename = path.basename(module.filename);
//依序讀取目錄中檔案,並且一一引入至同一物件中
fs.readdirSync(__dirname)
.filter((file) =>
(file.indexOf('.') !== 0) &&
(file !== basename) &&
(file.slice(-3) === '.js'))
.forEach((file) => {
const fileName = file.replace('.js','');
const route = require('./'+fileName);
allRoutes[fileName] = route;
});
//判斷非同步是否完成,完成後放入物件中。
Object.keys(allRoutes).forEach((routeName) => {
if (allRoutes[routeName].associate) {
allRoutes[routeName].associate(allRoutes);
}
});
module.exports = allRoutes;
```
### index.js
```javascript=1
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
const routers = require('./routes');
// 替app設定中介層為bodyParser
// 通過以下設定,在路由處理request,可以直接獲得body的部分
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
//獲取express Router物件
var topRouter = express.Router().get('/', function(req, res) {
res.json({ message: 'Hi,this is restFul API' });
});
//建立起第一層的router
app.use('/api', topRouter);
//註冊已經設計好的API
for(var key in routers) {
app.use('/api/'+key, routers[key]);
}
app.listen(3000);
console.log('伺服器運作中');
```
由根目錄的index.js將伺服器建立起來,負責所有路由的載入與設定,並監聽3000 port。
```
cd [yourDisk]:\rest-api
node index.js
```
在CMD中執行程式建立起整個伺服器。