Node.js RESTful API

前端工程師邁向後端之路 7 – 實作 Node.js RESTful API

Node.js RESTful API

在上一篇 前端工程師邁向後端之路 6 – 設計 RESTful API 我們設計好了我們的 API,目前 API 的示意圖如下(點圖可以放大):

Node.js RESTful API

在這篇我們就要來實作 Node.js RESTful API 的部分了。

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

執行完後如果有正確拿到資料就代表成功與資料庫連接了:

Node.js RESTful API

測試成功後就可以在 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:

Node.js RESTful API

按下 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:

Node.js RESTful API

可以觀察到 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:

Node.js RESTful API

可以看到我們的 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 要注意什麼

如果覺得我的文章有幫助的話,歡迎幫我的粉專按讚哦~謝謝你!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top