Node.js RESTful Web API 範例 for MySQL
在 Linux (CentOS 7) 使用 Node.js 搭配 Express 和 MySQL,建置 MVC 模式設計的 RESTful Web API 程式碼範例教學,並詳述 RESTful Web API 與 HTTP 方法和 HTTP 狀態碼的關係。
RESTful Web API 與 HTTP
REST (Representational State Transfer,表現層狀態轉換) 是架構在 HTTP (HyperText Transfer Protocol,超文本傳輸協定) 之上的設計,提供服務軟體的構建風格。
符合 REST 設計風格的 Web API 即稱為 RESTful API,它允許客戶端發出 URL (Uniform Resource Locator,統一資源定位符)存取或操作網路資源的請求。
HTTP 方法
依使用的 HTTP 方法 (method),來識別請求 (request) 資源 (source) 時要進行的操作。RESTful Web API 使用到的方法如下表:
方法 | 一組資源 URL:http://192.168.0.200:3000/accounts | 單個資源 URL:http://192.168.0.200:3000/accounts/2 |
---|---|---|
GET | 取得所有資源 | 取得指定的一筆資源 |
POST | 新增一筆資源 | |
PUT | 覆蓋指定的一筆資源 | |
PATCH | 更新指定的一筆資源 (部份更新) | |
DELETE | 刪除指定的一筆資源 |
HTTP 狀態碼
狀態碼 (Status Code) 是用來表示客戶端 (client) 發送請求至伺服器 (server) 進行處理後,伺服器回應 (response) 給客戶端的狀態,也就是讓客戶端用來識別請求是否被伺服器正確地處理。RESTful Web API 使用到的狀態碼如下表:
狀態碼 | 名稱 | 說明 | 備註 |
---|---|---|---|
200 | OK | 請求成功 | 狀態碼第一位數 2xx 類型為「成功」 |
201 | 201 Created | 新的資源已建立 | |
204 | No Content | 沒有內容 | |
400 | Bad Request | 請求不正確 | 狀態碼第一位數 4xx 類型為「客戶端原因引起的錯誤」 |
404 | Not Found | 沒有找到指定的資源 | |
410 | Gone | 指定的資源已不存在 | |
500 | Internal Server Error | 伺服器發生錯誤 | 狀態碼第一位數 5xx 類型為「伺服器原因引起的錯誤」 |
HTTP 方法匹配的狀態碼與內容
RESTful Web API 使用 HTTP 方法,請求成功 or 失敗匹配的狀態碼與內容如下表:
方法 | 請求成功 | 請求失敗 | ||
---|---|---|---|---|
狀態碼 | 內容 | |||
GET | 200 | 資源資料 | 404 | |
POST | 201 | 新增資源的 id | 400 | |
PUT | 200 | 該筆資源的完整資料 | 400 | 410 |
PATCH | 200 | 該筆資源更動的資料 | 400 | 410 |
DELETE | 204 | 沒有內容 | 400 | 410 |
資料庫
結構
本範例資料表結構如下表:
名稱 | 型態 | 空值 | 預設值 | AUTO_INCREMENT |
---|---|---|---|---|
id (Primary Key) | int(11) | ● | ||
username | char(30) | |||
password | char(40) | |||
role | enum('admin', 'user', 'guest') | ● | NULL |
SQL 語句
可執行以下 SQL 語法來建立本範例的資料表:
CREATE TABLE IF NOT EXISTS `accounts` (
`id` int(11) NOT NULL,
`username` char(30) COLLATE utf8_unicode_ci NOT NULL,
`password` char(40) COLLATE utf8_unicode_ci NOT NULL,
`role` enum('admin','user','guest') COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '角色權限'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
--
-- 資料表索引 `accounts`
--
ALTER TABLE `accounts`
ADD PRIMARY KEY (`id`);
--
-- 使用資料表 AUTO_INCREMENT `accounts`
--
ALTER TABLE `accounts`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
Node.js 前置作業
詳細使用說明可參考 Node.js 套件管理器 NPM 使用。
初始化專案
在專案目錄 (此例為 webapi) 自動生成 package.json 檔案:
npm init --yes
安裝模組
- body-parser - npm:解析 HTTP 請求主體的中介軟體。
- express - npm:快速建置 Web 應用程式架構。
- mysql - npm:連結 MySQL。
npm install body-parser express mysql
Node.js 程式架構
程式架構使用類似 MVC (Model–View–Controller) 模式設計,對應如下:
- models/ 目錄:MVC 模式的 Model (模型)。
- routes/ 目錄: MVC 模式的 Controller (控制器)。
webapi/ (專案目錄) │ ├── models/ (程式業務邏輯與資料庫存取) │ │ │ └── accounts.js │ ├── routes/ (負責轉發請求並回應結果) │ │ │ └── accounts.js │ ├── app.js (應用程式進入點) ├── conf.js (設定檔) └── functions.js (自訂 function)
Node.js 程式碼
設定檔
conf.js:
module.exports = {
db: {
host: 'localhost',
user: 'root',
password: '',
database: 'test'
},
port: 3000,
// 自訂密碼的加鹽
salt: '@2#!A9x?3'
};
自訂 function
functions.js:
var crypto = require('crypto'); // 加解密軟體 (內建模組)
var conf = require('./conf');
module.exports = {
// 將明文密碼加密
passwdCrypto: function (req, res, next) {
if (req.body.password) {
req.body.password = crypto.createHash('md5')
.update(req.body.password + conf.salt)
.digest('hex');
}
next();
}
};
應用程式進入點
app.js:
var bodyparser = require('body-parser'); // 解析 HTTP 請求主體的中介軟體
var express = require('express');
var conf = require('./conf');
var functions = require('./functions');
var accounts = require('./routes/accounts');
var app = express();
// 使用 bodyparser.json() 將 HTTP 請求方法 POST、DELETE、PUT 和 PATCH,放在 HTTP 主體 (body) 發送的參數存放在 req.body
app.use(bodyparser.urlencoded({ extended: false }));
app.use(bodyparser.json());
app.use(functions.passwdCrypto);
app.use('/accounts', accounts);
app.listen(conf.port, function () {
console.log('app listening on port ' + conf.port + '!');
});
routes
routes/ 目錄下的檔案程式 accounts.js:
var express = require('express');
var accounts = require('../models/accounts');
var router = express.Router();
// 獲取 /accounts 請求
router.route('/')
// 取得所有資源
.get(function (req, res) {
accounts.items(req, function (err, results, fields) {
if (err) {
res.sendStatus(500);
return console.error(err);
}
// 沒有找到指定的資源
if (!results.length) {
res.sendStatus(404);
return;
}
res.json(results);
});
})
// 新增一筆資源
.post(function (req, res) {
accounts.add(req, function (err, results, fields) {
if (err) {
res.sendStatus(500);
return console.error(err);
}
// 新的資源已建立 (回應新增資源的 id)
res.status(201).json(results.insertId);
});
});
// 獲取如 /accounts/1 請求
router.route('/:id')
// 取得指定的一筆資源
.get(function (req, res) {
accounts.item(req, function (err, results, fields) {
if (err) {
res.sendStatus(500);
return console.error(err);
}
if (!results.length) {
res.sendStatus(404);
return;
}
res.json(results);
});
})
// 刪除指定的一筆資源
.delete(function (req, res) {
accounts.delete(req, function (err, results, fields) {
if (err) {
res.sendStatus(500);
return console.error(err);
}
// 指定的資源已不存在
// SQL DELETE 成功 results.affectedRows 會返回 1,反之 0
if (!results.affectedRows) {
res.sendStatus(410);
return;
}
// 沒有內容 (成功)
res.sendStatus(204);
});
})
// 覆蓋指定的一筆資源
.put(function (req, res) {
accounts.put(req, function (err, results) {
if (err) {
res.sendStatus(500);
return console.error(err);
}
if (results === 410) {
res.sendStatus(410);
return;
}
accounts.item(req, function (err, results, fields) {
res.json(results);
});
});
})
// 更新指定的一筆資源 (部份更新)
.patch(function (req, res) {
accounts.patch(req, function (err, results, fields) {
if (err) {
res.sendStatus(500);
return console.error(err);
}
if (!results.affectedRows) {
res.sendStatus(410);
return;
}
// response 被更新的資源欄位,但因 request 主體的欄位不包含 id,因此需自行加入
req.body.id = req.params.id;
res.json([req.body]);
});
});
module.exports = router;
models
models/ 目錄下的檔案程式 accounts.js:
var mysql = require('mysql');
var conf = require('../conf');
var connection = mysql.createConnection(conf.db);
var sql = '';
module.exports = {
items: function (req, callback) {
sql = 'SELECT * FROM accounts';
return connection.query(sql, callback);
},
item: function (req, callback) {
sql = mysql.format('SELECT * FROM accounts WHERE id = ?', [req.params.id]);
return connection.query(sql, callback);
},
add: function (req, callback) {
sql = mysql.format('INSERT INTO accounts SET ?', req.body);
return connection.query(sql, callback);
},
delete: function (req, callback) {
sql = mysql.format('DELETE FROM accounts WHERE id = ?', [req.params.id]);
return connection.query(sql, callback);
},
put: function (req, callback) {
// 使用 SQL 交易功能實現資料回滾,因為是先刪除資料在新增,且 Key 值須相同,如刪除後發現要新增的資料有誤,則使用 rollback() 回滾
connection.beginTransaction(function (err) {
if (err) throw err;
sql = mysql.format('DELETE FROM accounts WHERE id = ?', [req.params.id]);
connection.query(sql, function (err, results, fields) {
// SQL DELETE 成功 results.affectedRows 會返回 1,反之 0
if (results.affectedRows) {
req.body.id = req.params.id;
sql = mysql.format('INSERT INTO accounts SET ?', req.body);
connection.query(sql, function (err, results, fields) {
// 請求不正確
if (err) {
connection.rollback(function () {
callback(err, 400);
});
} else {
connection.commit(function (err) {
if (err) callback(err, 400);
callback(err, 200);
});
}
});
} else {
// 指定的資源已不存在
callback(err, 410);
}
});
});
},
patch: function (req, callback) {
sql = mysql.format('UPDATE accounts SET ? WHERE id = ?', [req.body, req.params.id]);
return connection.query(sql, callback);
}
};
程式測試
安裝測試軟體
使用 Chrome 擴充功能安裝應用程式 Advanced REST client,可用它來測試伺服器提供的 RESTful Web API Service (服務)。
啟動程式並測試
執行 node
指令來啟動 Node.js 應用程式:
node app.js
app listening on port 3000
使用 Chrome 應用程式 Advanced REST client,發送 HTTP POST 請求方法,新增一筆資源 (這裡執行兩次,新增二筆資源):
發送 HTTP GET 請求方法,取得所有資源:
發送 HTTP GET 請求方法,取得指定的一筆資源:
發送 HTTP PUT 請求方法,覆蓋指定的一筆資源:
發送 HTTP PATCH 請求方法,更新指定的一筆資源 (部份更新):
發送 HTTP DELETE 請求方法,刪除指定的一筆資源:
錯誤排除
Error: listen EADDRINUSE
執行 node
指令啟動 Node.js 應用程式,顯示 Error: listen EADDRINUSE :::3000,表示網路通訊埠 (port) 3000 已被佔用,造成無法再使用相同的埠來啟動 Node.js 應用程式:
node app.js
events.js:183 throw er; // Unhandled 'error' event ^ Error: listen EADDRINUSE :::3000 at Object._errnoException (util.js:1022:11) at _exceptionWithHostPort (util.js:1044:20) at Server.setupListenHandle [as _listen2] (net.js:1351:14) at listenInCluster (net.js:1392:12) at Server.listen (net.js:1476:7) at Function.listen (/var/www/web-dev/webapi/node_modules/express/lib/application.js:618:24) at Object. (/var/www/web-dev/webapi/app.js:14:5) at Module._compile (module.js:643:30) at Object.Module._extensions..js (module.js:654:10) at Module.load (module.js:556:32)
使用 ss
指令查看網路狀態:
ss -ltnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 :::22 :::* users:(("sshd",pid=876,fd=4)) LISTEN 0 128 :::3000 :::* users:(("node" ,pid=3804,fd=11))
由上述得知網路埠 3000 被 node 佔用了,可使用 kill
指令的 -15
選項來正常終止一個 PID (Process IDentifier,程序 ID):
kill -15 3804
即可正常啟動 Node.js 應用程式:
node app.js
app listening on port 3000
參考
本著作係採用創用 CC 姓名標示-相同方式分享 3.0 台灣 授權條款授權.