在上一篇 前端工程師邁向後端之路 6 – 設計 RESTful API 我們設計好了我們的 API,目前 API 的示意圖如下(點圖可以放大):
在這篇我們就要來實作 Node.js RESTful API 的部分了。
Table of Contents
1. 連接 PostgreSQL server
1.1 Install pg dependency
在實作 RESTful API 之前,我們要先確保 Node.js 可以確實和 PostgreSQL server 溝通,因此要先安裝 node-postgres dependency:
$ yarn add pg
安裝完 dependency 後,可以根據官方文件建議的架構,在 src 底下新增一個 db 的資料夾,存放 db 相關的檔案,因為這是個小專案,所以這邊是新增一個 utils 資料夾,放 db 的檔案以及之後才會新增的 error-handler 等等。
1.2 新增 database.ts
// src/utils/database.ts
import { Pool, PoolConfig } from 'pg';
export class Database {
pool: Pool;
constructor(options: PoolConfig) {
this.pool = new Pool(options);
}
query(sql: string, params: any) {
return this.pool.query(sql, params);
}
}
當這個 Database class 被 new 一個 instance 後,就會根據 config 檔連到目標 PostgreSQL server,接著我們在 index.ts 裡測試是否可以針對資料庫做操作:
1.3 測試 SQL
// src/test.ts
import { Database } from './utils/database';
export const database = new Database({
host: 'localhost',
port: 5432,
database: 'restful-tutorial',
user: 'jimmy2952',
password: ''
});
(async () => {
const sql = `
SELECT * FROM users;
`;
const result = await database.query(sql, []);
console.log(result.rows);
})();
因為在前幾篇的:前端工程師邁向後端之路 5 – PostgreSQL migration:淺談 database migration ( 資料庫遷移 )?已經有在資料庫 init 資料了,所以照理說如果正確執行 SQL 的話可以拿到三個 user 的資料,接著就用 ts-node 直接執行這個 ts 檔來確認 SQL 是否可以正確地拿回資料吧!
要注意因為我們沒有在 global 安裝 ts-node,因此無法直接執行 ts-node src/database-test.ts,需要到 node-modules 找到 ts-node 的指令檔案。
$ node_modules/ts-node/dist/bin.js src/test.ts
執行完後如果有正確拿到資料就代表成功與資料庫連接了:
測試成功後就可以在 index.ts new 一個 Database instance,代表啟動 server 時,會先與 database server 連接:
// src/index.ts
import express, { Request, Response } from 'express';
import { Database } from './utils/database';
const app = express();
const port = 3000;
export const database = new Database({
host: 'localhost',
port: 5432,
database: 'restful-tutorial',
user: 'jimmy2952',
password: ''
});
app.get('/', (req: Request, res: Response) => {
res.send('The server is working!');
});
app.listen(port, () => {
console.log(`server is listening on ${port} !!!`);
});
這邊要 export 是因為等等在其他地方會需要用到 Database 裡面的 query function。
2. 定義 routes
2.1 新增 route
記得我們在 RESTful API 的示意圖有提到每一個 router 基本上會對應到特定的 function 嗎,我們可以先寫出第一個 route 以及其對應的 function,在 index.ts 中新增以下的 code:
// src/index.ts
app.get('/api/users/:id', async (req: Request, res: Response, next: NextFunction) => {
// 取得 url 中的 id 參數
const { id } = req.params;
const sql = `
SELECT * FROM users WHERE id = $1;
`;
const params = [id];
// 使用 async await 的時候記得要在外面包一層 try catch,發生 error 的時候才 catch 的到
try {
const result = await database.query(sql, params);
if (result !== null && result.rowCount !== 0) {
res.status(200);
res.send(result.rows[0]);
} else if (result.rowCount === 0) {
const error = new ErrorHandler('Cannot find user', 404);
next(error);
}
} catch (err) {
// 如果發生 error 先 console 出來,之後會再寫一個 error-handler 來處理所有 error
console.log(err);
}
});
這邊你可能會覺得有點奇怪,為什麼 SQL 的部分不直接寫 SELECT * FROM users WHERE id = ${id};
就好了,而是用參數的方式帶進去?這是因為如果不用參數的方式帶進去很有可能會有 SQL Injection 的資安問題,想多了解什麼是 SQL Injection ,可以參考這邊文章:網站安全🔒 一次看懂 SQL Injection 的攻擊原理 — 「雍正繼位之謎」。
這麼一來代表如果有一個 http request 的 url 為:http://localhost:3000/api/users/:id,而且是 get method 的話,就會執行後面那個 function 去資料庫拿資料。
2.2 Send http request
接著我們可以用 Postman 來測試 http request:
按下 Send 後有確實從 http response 拿回我們在 database 的資料,代表成功了!
2.3 Refactor routes
在真實的專案中通常不太只會有幾個 routes,因此一般來說會將 routes 統一放在一個資料夾,所以這邊就新增一個 routes 的資料夾,在這個資料夾底下再新增一個 users-routes.ts:
// src/routes/users-routes.ts
import {
NextFunction, Request, Response, Router
} from 'express';
import { database } from '../index';
export class UsersApi {
public router: Router;
constructor() {
this.router = Router();
this.router.get('/users', async (req: Request, res: Response, next: NextFunction) => {
const sql = `
SELECT * FROM users;
`;
const params = [];
try {
const result = await database.query(sql, params);
if (result !== null && result.rowCount !== 0) {
res.status(200);
res.send(result.rows);
}
} catch (err) {
console.log(err);
}
});
}
}
然而當 routes 的數量變多時,整個 routes 的檔案也會變大,因此通常會將每個 routes 對應到的 function (稱為 controllers),在獨立放到一個 controllers 的資料夾,以方便維護。
3. 新增 controllers
3.1 新增 users-controllers.ts
在 src 底下新增一個 controllers 資料夾,再新增一個 users-controllers.ts:
// src/controllers/users-controllers.ts
import { NextFunction, Request, Response } from 'express';
import { database } from '../index';
import { ErrorHandler } from '../utils/error-handler';
export async function getUsers(req: Request, res: Response, next: NextFunction) {
const sql = `
SELECT * FROM users;
`;
const params = [];
try {
const result = await database.query(sql, params);
if (result !== null && result.rowCount !== 0) {
res.status(200);
res.send(result.rows);
}
} catch (err) {
console.log(err);
}
}
export async function getUser(req: Request, res: Response, next: NextFunction) {
const { id } = req.params;
const sql = `
SELECT * FROM users WHERE id = $1;
`;
const params = [id];
try {
const result = await database.query(sql, params);
if (result !== null && result.rowCount !== 0) {
res.status(200);
res.send(result.rows[0]);
}
} catch (err) {
console.log(err);
}
}
export async function createUser(req: Request, res: Response, next: NextFunction) {
const sql = `
INSERT INTO users (username, email)
VALUES ($1, $2);
`;
const { username, email } = req.body;
const params = [username, email];
try {
const result = await database.query(sql, params);
if (result !== null && result.rowCount !== 0) {
res.sendStatus(201);
}
} catch (err) {
console.log(err);
}
}
export async function updateUser(req: Request, res: Response, next: NextFunction) {
const sql = `
UPDATE users
SET username = $1, email = $2
WHERE id = $3;
`;
const { id } = req.params;
const { username, email } = req.body;
const params = [username, email, id];
const result = await database.query(sql, params);
try {
const result = await database.query(sql, params);
if (result !== null && result.rowCount !== 0) {
res.sendStatus(204);
}
} catch (err) {
console.log(err);
}
}
export async function deleteUser(req: Request, res: Response, next: NextFunction) {
const sql = `
DELETE FROM users WHERE id = $1;
`;
const { id } = req.params;
const params = [id];
const result = await database.query(sql, params);
try {
const result = await database.query(sql, params);
if (result !== null && result.rowCount !== 0) {
res.sendStatus(201);
}
} catch (err) {
console.log(err);
}
}
將所有的邏輯都放在 controllers,這樣就算以後專案的 API 變多時,也不會太難維護。
3.2 改寫 users-routes.ts
而在 routes 的部分則可以直接 import 這些 function 來使用:
// src/routes/users-routes.ts
import { Router } from 'express';
import {
getUsers, getUser, createUser, updateUser, deleteUser
} from '../controllers/users-controllers';
export class UsersApi {
public router: Router;
constructor() {
this.router = Router();
this.router.get('/users', getUsers);
this.router.get('/users/:id', getUser);
this.router.post('/users', createUser);
this.router.patch('/users/:id', updateUser);
this.router.delete('/users/:id', deleteUser);
}
}
3.3 改寫 index.ts
將 routes 和 controllers 都獨立出來後,就可以在 index.ts import 進來了:
// src/index.ts
export const database ...
const usersApi = new UsersApi();
app.use(express.json());
app.use('/api', usersApi.router);
這邊加上 express.json() 的原因可以參考:【 Node.js 】為什麼要使用 express bodyparser 呢?,request 的 body 必須加上 body parser 才可以在 Node.js 裡拿的到。
4. 新增 error-handler
當 server 發生問題時,該如何處理 error 也是個非常重要的事情,目前我們只有用 try catch 把 error 給 console 出來,但使用者方面則完全沒有收到 http response,因此不知道到底是 server 端發生問題,還是自己發送的 http request 有誤等等。
可以試著把 PostgreSQL server 給關掉,再用 Postman 來送 http request:
可以觀察到 client 端會一直處於等待 response 的狀態,不知道自己的 request 到底有沒有被正確接收或是 server 端發生什麼問題。
4.1 新增 error-handler.ts
通常發生 error 時,可以使用 Node.js 內建的 Error object 來處理 error,但我們目前是在開發 web app,因此通常會配合使用 http status code 來告訴使用者發生什麼事,因此我們可以 extends Error object 來客製化我們的 Error class,在 utils 資料夾底下新增 error-handler.ts:
// src/utils/error-handler.ts
export class ErrorHandler extends Error {
code: number;
constructor(message: string = 'Internal Server Error.', errorCode: number = 500) {
super(message);
this.code = errorCode;
}
}
這麼一來當 new 一個 ErrorHandler class 的時候,就可以把 error message 和 status code 傳進這個 class。
4.2 改寫 controllers
將 try catch 的 catch block 改寫為下面這樣:
catch (err) {
const error = new ErrorHandler();
next(error);
}
如果要針對個別的 case 傳送不同的 message 和 status code 則可以寫成:
catch (err) {
const error = new ErrorHandler('username is duplicate, please choose other one', 400);
next(error);
}
4.3 在 index.ts 處理 error
app.use(express.json());
app.use('/api', usersApi.router);
app.use((error: ErrorHandler, req: Request, res: Response, next: NextFunction) => {
if (res.headersSent) {
next(error);
}
res.status(error.code || 500);
res.json({
code: error.code || 500,
message: error.message || 'An unknown error occurred!',
stack: error.stack || {}
});
});
要注意的是處理 error 的 middleware 一定要在所有的 routes 後面,這樣子在任何 routes call next(error) 才會進到這個 middleware 來處理 error。
4.4 Send http request 測試 error handler
重複剛剛的步驟,用 Postman 再 send 一次 http request:
可以看到我們的 middleware 確實的把 error 回傳給 client 端了!
這麼一來就完成了最基本的 RESTful API 了,完整的 code 可以參考我的 GitHub。
另外推薦其他幾篇 Node.js RESTful 的系列文:
從無到有打造 RESTful API service
Node.js 從無到有,打造一個漂亮乾淨俐落的 RESTful API
API 實作(一):規劃 RESTful API 要注意什麼
參考資料
Welcome | node-postgres
網站安全🔒 一次看懂 SQL Injection 的攻擊原理 — 「雍正繼位之謎」
用 Node.js 快速打造 RESTful API
從無到有打造 RESTful API service
Node.js 從無到有,打造一個漂亮乾淨俐落的 RESTful API
API 實作(一):規劃 RESTful API 要注意什麼
如果覺得我的文章有幫助的話,歡迎幫我的粉專按讚哦~謝謝你!