前言

剛從 Laravel 跳過來 express,非常喜歡它的簡潔以及靈活的架構,不過在寫後端程式時,還是希望能有 OO 的架構,因此嘗試使用 TypeScript 來開發,本文記錄從安裝、開發到專案架構的一些筆記。

使用 TypeScript 的好處

  1. 提供許多 OO pattern 的方法,如介面、繼承與抽象類別
  2. 強型別,能夠在編譯過程中先行找到一些錯誤
  3. 能夠編譯成不同版本的 JS
  4. 有 Declaration files 能夠使用 JS Library,因此不太需要擔心套件相容問題,像是 express 有 @type/express

一開始的檔案架構

在這邊有一個簡單的骨架給大家使用,除了原本的 express 和 typescript 外,還包含了:

  1. nodeman — 用來監聽檔案改變並重開 dev server
  2. 兩個 declaration packages,這些定義套件都會以 @types/package 為名
1
git clone https://github.com/kusakawazeusu/express-typescript-skeleton.git project

接著下指令來開啟 dev server:

1
2
yarn dev
# or npm run dev

這時候 tsc 已經會開始監看 src 資料夾的檔案變化,編譯後的檔案會被存放在 build 資料夾中,這時候在網址列打入 http://localhost:3000 就可以看到回傳的 index 字串囉!

編譯器設定

在專案資料夾中,有一個 tsconfig.json 檔案,可以設定輸入輸出的資料夾路徑,以及編譯模組等⋯⋯。


Controller

請求經過 router 分配過後,會交由 controller 負責處理,並回傳對應的 response,我們先寫一個只會單純回應字串的單純 Controller:

1
2
3
4
5
6
7
8
9
10
// src/controllers/AuthController.ts
import {Request, Response} from "express";

class AuthController {
echo(req: Request, res: Response) {
res.send('echo');
}
}

export default AuthController;

Route

在這裡,我們要將使用者的請求,依照其進入的 Url 分配給不同的 Controller,像是從 GET /login 進來的請求,我們希望交給上面寫好的 echo ,並回傳一個固定字串。

  1. 先寫出 route 的 abstract class:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // src/routes/route.ts

    import {Router} from "express";

    abstract class Route {
    protected router = Router();
    protected abstract setRoutes(): void;

    public getRouter() {
    return this.router;
    }
    }

    export default Route;
  2. 新增 AuthRoute 並寫上 url 與 controller 的映射關係:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // src/routes/auth.route.ts

    import AuthController from "../controllers/AuthController"
    import Route from "./route";

    class AuthRoute extends Route{
    private authController = new AuthController();

    constructor() {
    super();
    this.setRoutes();
    }

    protected setRoutes() {
    this.router.get('/login', this.authController.echo);
    }
    }

    export default AuthRoute;
  3. 新增一個 router.ts 檔案,輸出一個陣列以供 app.ts 載入:

    1
    2
    3
    4
    5
    6
    7
    8
    // src/router.ts

    import Route from "./routes/route";
    import AuthRoute from "./routes/auth.route";

    export const router: Array<Route> = [
    new AuthRoute(),
    ];
  4. 在 app.ts 中載入 router

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // src/app.ts

    import express from 'express';
    import morgan from 'morgan';
    import {router} from "./router";

    const app: express.Application = express();

    app.use(morgan('dev'));
    app.use(express.json());
    app.use(express.urlencoded({ extended: false }));

    // load router
    for (const route of router) {
    app.use(route.getRouter());
    }

    module.exports = app;

此時在瀏覽器上打 http://localhost:3000/login ,就可以看到回傳的 echo 字串,總結一下,當你今天要新增一個 route 的時候,你需要以下步驟:

  1. 新增一個 Controller,並撰寫相關邏輯
  2. 新增一個 Route 的衍生類別,並在 setRoutes 函式中將 url 與 method 對應到 controller
  3. 在 router.ts 加入剛剛新增的 Route class

Prefix

同一個 route 檔案常常會有相同的 prefix url,例如說 auth route 可能會有:

  • POST auth/login
  • POST auth/logout
  • POST auth/forgetPassword

我們在 Route class 中加入一個新的資料成員 prefix,用來設定每個 route 的前綴網址:

1
2
3
4
5
6
7
// src/routes/route.ts

protected prefix: string = '/';

public getPrefix() {
return this.prefix;
}

然後在 AuthRoute 的建構子修改它:

1
2
3
4
5
6
7
// src/routes/auth.route.ts

constructor() {
super();
this.prefix = '/auth';
this.setRoutes();
}

最後,在 app.ts 中,載入 router 時加入 prefix 的設定:

1
2
3
4
5
// src/app.ts

for (const route of router) {
app.use(route.getPrefix(), route.getRouter());
}

如此一來,原本是 /login 的 url,在加上 prefix 之後,就會變成 /auth/login,而其他在 AuthRoute 定義的 url 也都會變成 /auth/*


Middleware

在 Express 中,有三種可以嵌套 middleware 的方式:

  1. 應用到單一 url
  2. 應用到單一 route 檔案
  3. 全域使用,每個請求都會經過這個 middleware

我們先寫一個簡單的 middleware,他只看請求的 header 裡面有沒有 Authorization,若沒有的話會回傳 status code 401:

1
2
3
4
5
6
7
8
9
10
11
// src/middleware/AuthMiddleware.ts

import {Request, Response, NextFunction} from "express";

export function AuthMiddleware(req: Request, res: Response, next: NextFunction) {
if (!req.header('Authorization')) {
return res.status(401).send('unauthorized');
}

next();
}

若你想套用在單一 url,只需要放在 router.METHOD 的第二個參數即可:

1
2
3
// src/routes/auth.route.ts

this.router.get('/login', AuthMiddleware, this.authController.echo);

套用在單一 route 檔案,則在該 route class 的建構子加入(必須在 setRoutes() 之前):

1
2
3
// src/routes/auth.route.ts

this.router.use(AuthMiddleware);

全域使用的話,就在 app.ts 裡面加上:

1
2
3
// src/app.ts

app.use(AuthMiddleware);

Validator

若要驗證 Request body 或 query string 的內容,建議可以使用 express-validator 套件,裡面有各式各樣的驗證用 middleware 可供使用。

1
yarn add @types/express-validator express-validator

假設我們的登入表單需要有 username 和 password 兩個欄位,且最少要有四個字,我們能夠將這個驗證寫成 request 檔案:

1
2
3
4
5
6
7
8
9
10
// src/requests/AuthRequest.ts

import {check} from "express-validator";
import {showApiError} from "../middleware/AuthMiddleware";

export const loginRequest = [
check('username').exists().isLength({min: 4}),
check('password').exists().isLength({min: 4}),
showApiError
];

showApiError 是為了在有 input 錯誤發生時,能夠回傳對應的錯誤訊息而使用的 middleware,如果沒有它的話,validator 不會回傳錯誤訊息,內容如下:

1
2
3
4
5
6
7
8
9
10
import {validationResult} from "express-validator";

export function showApiError(req: Request, res: Response, next: NextFunction) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

next();
}

接著我們在 auth route class 裡面載入這個 request:

1
2
3
4
5
6
7
// src/routes/auth.route.ts

import {loginRequest} from "../requests/AuthRequest";

protected setRoutes() {
this.router.post('/login', loginRequest, this.authController.echo);
}

如此一來 POST /login 的請求就會驗證 request body,當我的 username 輸入太短時,會得到以下錯誤:

1
2
3
4
5
6
7
8
9
10
{
"errors": [
{
"value": "123",
"msg": "Invalid value",
"param": "username",
"location": "body"
}
]
}

因為 request 其實就是 middleware 的陣列,因此若要嵌套其他 middleware 時,使用 Array merge 即可。

寫到這裡,我們的 src 資料夾架構應該要長得像下面這樣子,目前已經可以寫一些靜態的 API,下一篇我們會使用 Sequelize 來連結資料庫。

1
2
3
4
5
6
7
/src
controllers/
middleware/
requests/
routes/
app.ts
router.ts

結論

好想寫一個 CLI⋯⋯然後取名 artisan(?)。


喜歡這篇文章嗎?

歡迎點擊按鈕分享到 Facebook 上唷!

Weightless Theme
Rocking Basscss