前言
剛從 Laravel 跳過來 express,非常喜歡它的簡潔以及靈活的架構,不過在寫後端程式時,還是希望能有 OO 的架構,因此嘗試使用 TypeScript 來開發,本文記錄從安裝、開發到專案架構的一些筆記。
使用 TypeScript 的好處
- 提供許多 OO pattern 的方法,如介面、繼承與抽象類別
- 強型別,能夠在編譯過程中先行找到一些錯誤
- 能夠編譯成不同版本的 JS
- 有 Declaration files 能夠使用 JS Library,因此不太需要擔心套件相容問題,像是 express 有 @type/express
一開始的檔案架構
在這邊有一個簡單的骨架給大家使用,除了原本的 express 和 typescript 外,還包含了:
- nodeman — 用來監聽檔案改變並重開 dev server
- 兩個 declaration packages,這些定義套件都會以 @types/package 為名
git clone https://github.com/kusakawazeusu/express-typescript-skeleton.git project
接著下指令來開啟 dev server:
yarn dev
# or npm run dev
這時候 tsc 已經會開始監看 src 資料夾的檔案變化,編譯後的檔案會被存放在 build 資料夾中,這時候在網址列打入 http://localhost:3000 就可以看到回傳的 index 字串囉!
編譯器設定
在專案資料夾中,有一個 tsconfig.json 檔案,可以設定輸入輸出的資料夾路徑,以及編譯模組等⋯⋯。
Controller
請求經過 router 分配過後,會交由 controller 負責處理,並回傳對應的 response,我們先寫一個只會單純回應字串的單純 Controller:
// src/controllers/AuthController.tsimport {Request, Response} from "express";class AuthController {
echo(req: Request, res: Response) {
res.send('echo');
}
}export default AuthController;
Route
在這裡,我們要將使用者的請求,依照其進入的 Url 分配給不同的 Controller,像是從 GET /login 進來的請求,我們希望交給上面寫好的 echo ,並回傳一個固定字串。
- 先寫出 route 的 abstract class:
// src/routes/route.tsimport {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 的映射關係:
// src/routes/auth.route.tsimport 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 載入:
// src/router.tsimport Route from "./routes/route";
import AuthRoute from "./routes/auth.route";export const router: Array<Route> = [
new AuthRoute(),
];
4. 在 app.ts 中載入 router
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 的時候,你需要以下步驟:
- 新增一個 Controller,並撰寫相關邏輯
- 新增一個 Route 的衍生類別,並在 setRoutes 函式中將 url 與 method 對應到 controller
- 在 router.ts 加入剛剛新增的 Route class
Prefix
同一個 route 檔案常常會有相同的 prefix url,例如說 auth route 可能會有:
- POST auth/login
- POST auth/logout
- POST auth/forgetPassword
- …
我們在 Route class 中加入一個新的資料成員 prefix,用來設定每個 route 的前綴網址:
// src/routes/route.tsprotected prefix: string = '/';public getPrefix() {
return this.prefix;
}
然後在 AuthRoute 的建構子修改它:
// src/routes/auth.route.tsconstructor() {
super();
this.prefix = '/auth';
this.setRoutes();
}
最後,在 app.ts 中,載入 router 時加入 prefix 的設定:
// src/app.tsfor (const route of router) {
app.use(route.getPrefix(), route.getRouter());
}
如此一來,原本是 /login 的 url,在加上 prefix 之後,就會變成 /auth/login,而其他在 AuthRoute 定義的 url 也都會變成 /auth/*。
Middleware
在 Express 中,有三種可以嵌套 middleware 的方式:
- 應用到單一 url
- 應用到單一 route 檔案
- 全域使用,每個請求都會經過這個 middleware
我們先寫一個簡單的 middleware,他只看請求的 header 裡面有沒有 Authorization,若沒有的話會回傳 status code 401:
// src/middleware/AuthMiddleware.tsimport {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 的第二個參數即可:
// src/routes/auth.route.tsthis.router.get('/login', AuthMiddleware, this.authController.echo);
套用在單一 route 檔案,則在該 route class 的建構子加入(必須在 setRoutes() 之前):
// src/routes/auth.route.tsthis.router.use(AuthMiddleware);
全域使用的話,就在 app.ts 裡面加上:
// src/app.tsapp.use(AuthMiddleware);
Validator
若要驗證 Request body 或 query string 的內容,建議可以使用 express-validator 套件,裡面有各式各樣的驗證用 middleware 可供使用。
yarn add @types/express-validator express-validator
假設我們的登入表單需要有 username 和 password 兩個欄位,且最少要有四個字,我們能夠將這個驗證寫成 request 檔案:
// src/requests/AuthRequest.tsimport {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 不會回傳錯誤訊息,內容如下:
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:
// src/routes/auth.route.tsimport {loginRequest} from "../requests/AuthRequest";protected setRoutes() {
this.router.post('/login', loginRequest, this.authController.echo);
}
如此一來 POST /login 的請求就會驗證 request body,當我的 username 輸入太短時,會得到以下錯誤:
{ "errors": [ { "value": "123", "msg": "Invalid value", "param": "username", "location": "body" } ]}
因為 request 其實就是 middleware 的陣列,因此若要嵌套其他 middleware 時,使用 Array merge 即可。
寫到這裡,我們的 src 資料夾架構應該要長得像下面這樣子,目前已經可以寫一些靜態的 API,下一篇我們會使用 Sequelize 來連結資料庫。
/src
controllers/
middleware/
requests/
routes/
app.ts
router.ts
結論
好想寫一個 CLI⋯⋯然後取名 artisan(?)。