前幾天我們已經使用 Express 完成 Notes API 的基本 CRUD,現在的 app.js 已經可以處理查詢、新增、修改與刪除 Note。
不過隨著 API 越來越多,所有 Route 都繼續放在同一個檔案裡,程式也會開始變得難以整理。
例如目前只有 Notes API,所以把這些 Route 都寫在 app.js 裡還不算複雜:
GET /notes
GET /notes/:id
POST /notes
PATCH /notes/:id
DELETE /notes/:id
但之後可能還會加入 Users、Auth、Products、Orders 等功能。如果所有 Route 都繼續放在 app.js,最後可能會變成:
app.js
├─ Notes API
├─ Users API
├─ Auth API
├─ Products API
└─ Orders API
當不同功能的程式碼全部集中在同一個檔案裡,除了檔案會越來越長,也不容易快速找到自己正在處理的功能。因此今天要學習的,就是如何按照功能拆分 Route,讓不同功能各自管理自己的 Route。
在開始拆分之前,可以先簡單區分幾個 API 開發中很常看到的名稱。URL 是完整的網址,代表前端實際請求的位置,例如:
http://localhost:3000/notes/123
Endpoint 可以理解成 API 對外提供的一個具體入口,例如 GET /notes 或 GET /notes/:id。Route 則是後端定義「收到這個請求之後,要執行哪段程式」的規則,例如:
app.get("/notes", (req, res) => {
// ...
});
這段程式就是在 Express 中建立一個 Route。實際開發時,URL、Endpoint、Route 這幾個名稱有時候會混著使用,所以不用過度糾結細微差異,先知道 URL 是實際網址、Endpoint 是 API 提供的入口,而 Route 是後端處理請求的規則即可。
一開始直接把 Route 寫在 app.js 裡,其實完全沒有問題。目前的 Notes API 可以直接寫成:
const express = require("express");
const app = express();
app.use(express.json());
app.get("/notes", (req, res) => {
// 取得所有 Notes
});
app.get("/notes/:id", (req, res) => {
// 取得單筆 Note
});
app.post("/notes", (req, res) => {
// 新增 Note
});
app.patch("/notes/:id", (req, res) => {
// 修改 Note
});
app.delete("/notes/:id", (req, res) => {
// 刪除 Note
});
app.listen(3000);
目前只有五個 Route,所以還算容易閱讀。但當功能開始增加,Notes、Users、Auth 等功能都會有自己的 Route,如果全部集中在 app.js,不同功能的程式碼就會交錯在一起,檔案也會越來越長。
因此,可以按照功能把 Route 拆成不同的 Router。Router 可以理解成「一組可以獨立管理的 Route」,例如 Notes 有自己的 notesRouter,Users 有自己的 usersRouter,Auth 也有自己的 authRouter,最後再由 app.js 把這些 Router 掛載到對應的路徑。
整體關係可以理解成:
app.js
├─ /notes → notesRouter
│ ├─ GET /
│ ├─ GET /:id
│ ├─ POST /
│ ├─ PATCH /:id
│ └─ DELETE /:id
│
├─ /users → usersRouter
│ ├─ GET /
│ ├─ GET /:id
│ └─ POST /
│
├─ /auth → authRouter
│ ├─ POST /login
│ └─ POST /logout
│
└─ /products → productsRouter
├─ GET /
├─ GET /:id
└─ POST /
這樣 app.js 主要負責整個 Express App 的設定,以及決定不同功能由哪一個 Router 負責;每個 Router 則專心管理自己的 Route。
Express 可以透過 express.Router() 建立 Router。建立之後,Router 就可以使用 get()、post()、patch()、delete() 等方法定義 Route,使用方式和 app.get()、app.post() 很接近,差別在於 app 代表整個 Express App,而 router 則是一組可以獨立管理的 Route。
例如原本直接在 App 上建立:
app.get("/notes", (req, res) => {
// ...
});
現在可以把 Notes 相關的 Route 移到 Router:
router.get("/", (req, res) => {
// ...
});
這時候先不要把 /notes 寫進 Router,因為 /notes 會在 Router 掛載時統一處理。這樣同一個 Router 裡的 Route 就只需要描述自己底下的路徑。
先建立專案結構:
project/
├─ app.js
└─ routes/
└─ notes.js
接著在 routes/notes.js 中建立 Router,並把前幾天完成的 Notes CRUD 移進來:
const express = require("express");
const router = express.Router();
router.get("/", (req, res) => {
// 取得所有 Notes
});
router.get("/:id", (req, res) => {
// 取得單筆 Note
});
router.post("/", (req, res) => {
// 新增 Note
});
router.patch("/:id", (req, res) => {
// 修改 Note
});
router.delete("/:id", (req, res) => {
// 刪除 Note
});
module.exports = router;
這裡的 CRUD 邏輯基本上沒有改變,主要只是把原本由 app 管理的 Route 改成由 router 管理。原本的 /notes 也先拿掉,留下 / 和 /:id,讓共同的路徑前綴可以在 App 層統一設定。
做到這裡會看到 module.exports = router,因為 Router 已經被拆到 routes/notes.js,app.js 也需要取得這個 Router。不同檔案之間要互相使用程式碼,就需要透過模組系統來匯入與匯出內容。
Node.js 目前支援 CommonJS 與 ES Module 兩種主要的模組系統。CommonJS 使用 require() 引入模組,使用 module.exports 匯出內容;ES Module 則使用 import 與 export。
CommonJS:
// 引入
const express = require("express");
// 匯出
module.exports = router;
ES Module:
// 引入
import express from "express";
// 匯出
export default router;
兩種寫法都能完成模組化,只是語法與模組規則不同。本系列前面的 Node.js 程式碼已經使用 CommonJS,因此這篇會延續 require() 與 module.exports 的寫法,避免在學習 Router 的同時又需要切換另一套模組語法。
在 CommonJS 中,如果一個檔案主要就是提供一個內容,可以直接使用 module.exports 匯出。例如目前的 notes.js 主要提供一個 Router,所以:
module.exports = router;
就能將 Router 提供給其他檔案使用。接著 app.js 就可以透過 require() 將它引入:
const notesRouter = require("./routes/notes");
如果一個檔案需要提供多個相關內容,也可以使用物件:
module.exports = {
add,
subtract,
};
或者:
exports.add = add;
exports.subtract = subtract;
這些方式比較適合一個檔案需要提供多個功能的情況,而目前的 notes.js 主要就是提供一個 Router,因此使用 module.exports = router 會比較直觀。
把這個流程串起來看,就是 routes/notes.js 負責建立 Router,再透過 module.exports 匯出;app.js 則透過 require() 引入這個 Router。這樣兩個檔案就可以分工,而不需要把所有程式碼重新寫在同一個檔案裡。
app.use() 掛載 RouterRouter 引入 app.js 之後,還需要告訴 Express 這個 Router 要負責哪一段路徑,這時候就會使用 app.use()。app.use() 除了可以掛載 Middleware,也可以用來掛載 Router。現在我們要讓 notesRouter 負責 /notes 底下的所有 Route,因此可以寫成:
const express = require("express");
const notesRouter = require("./routes/notes");
const app = express();
app.use(express.json());
app.use("/notes", notesRouter);
app.listen(3000);
這裡的 "/notes" 是 Router 的共同路徑前綴,而 notesRouter 則負責這個路徑底下的 Route。因此 routes/notes.js 中的:
router.get("/");
router.get("/:id");
router.post("/");
router.patch("/:id");
router.delete("/:id");
搭配:
app.use("/notes", notesRouter);
之後,實際對外提供的 API 就會是:
GET /notes
GET /notes/:id
POST /notes
PATCH /notes/:id
DELETE /notes/:id
也就是說,Router 裡的 / 不代表最後的完整 URL,而是會加上 app.use() 所設定的 /notes。
這樣的設計可以把共同的路徑集中管理。例如未來想把 Notes API 改成 /api/notes,只需要修改:
app.use("/api/notes", notesRouter);
Router 裡的程式碼不需要跟著修改,原本的:
router.get("/");
就會變成:
GET /api/notes
這也是 Router 很重要的一個好處:共同的路徑由 App 管理,而 Router 只需要處理自己的 Route。
拆分之前,app.js 同時負責 Express App 設定與 Notes API:
const express = require("express");
const app = express();
app.use(express.json());
app.get("/notes", (req, res) => {
// 取得所有 Notes
});
app.get("/notes/:id", (req, res) => {
// 取得單筆 Note
});
app.post("/notes", (req, res) => {
// 新增 Note
});
app.patch("/notes/:id", (req, res) => {
// 修改 Note
});
app.delete("/notes/:id", (req, res) => {
// 刪除 Note
});
app.listen(3000);
拆分之後,app.js 只需要負責建立 App、設定 Middleware,以及掛載 Notes Router:
const express = require("express");
const notesRouter = require("./routes/notes");
const app = express();
app.use(express.json());
app.use("/notes", notesRouter);
app.listen(3000);
Notes 相關的 Route 則集中在:
routes/
└─ notes.js
因此拆分前後,API 本身沒有改變:
拆分前
app.js
├─ GET /notes
├─ GET /notes/:id
├─ POST /notes
├─ PATCH /notes/:id
└─ DELETE /notes/:id
拆分後:
app.js
└─ /notes → notesRouter
├─ GET /
├─ GET /:id
├─ POST /
├─ PATCH /:id
└─ DELETE /:id
GET /notes 還是 GET /notes,POST /notes 也還是 POST /notes,改變的不是 API 本身,而是 Route 的管理方式。原本集中在 app.js 的 Notes Route,現在交給 notesRouter 管理,讓兩個檔案各自負責比較明確的工作。
今天先把 Notes 拆成自己的 Router,之後 Users、Auth 或其他功能也可以使用相同的方式。例如建立:
routes/
├─ notes.js
├─ users.js
└─ auth.js
再由 app.js 統一掛載:
app.use("/notes", notesRouter);
app.use("/users", usersRouter);
app.use("/auth", authRouter);
之後整體結構就會變成:
app.js
├─ /notes → notesRouter
│ ├─ GET /
│ ├─ GET /:id
│ ├─ POST /
│ ├─ PATCH /:id
│ └─ DELETE /:id
│
├─ /users → usersRouter
│ ├─ GET /
│ ├─ GET /:id
│ └─ POST /
│
└─ /auth → authRouter
├─ POST /login
└─ POST /logout
當功能越來越多時,就可以使用相同的方式,把不同功能的 Route 分別放進自己的 Router。app.js 負責整體的組合與設定,各個 Router 則負責自己的 Route,這樣專案即使持續增加功能,也比較容易維持清楚的結構。
完成今天的內容後,專案結構會變成:
project/
├─ app.js
└─ routes/
└─ notes.js
routes/notes.js 負責 Notes CRUD,app.js 則透過 require() 引入 notesRouter,再使用 app.use("/notes", notesRouter) 將它掛載到 /notes。
這次重構沒有改變 API 本身,只是把原本集中在 app.js 的 Route 拆開管理,從這裡開始,當 API 越來越多時,就可以按照功能建立不同 Router,讓每個功能都有自己的位置。