iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
佛心分享-IT 人自學之術

出發吧!後端菜鳥:30 天的後端學習紀錄系列 第 9

Day 09|API 越寫越多怎麼辦?用 Router 拆分路由

  • 分享至 

  • xImage
  •  

前言

前幾天我們已經使用 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。

URL、Endpoint、Route 有什麼不同?

在開始拆分之前,可以先簡單區分幾個 API 開發中很常看到的名稱。URL 是完整的網址,代表前端實際請求的位置,例如:

http://localhost:3000/notes/123

Endpoint 可以理解成 API 對外提供的一個具體入口,例如 GET /notesGET /notes/:id。Route 則是後端定義「收到這個請求之後,要執行哪段程式」的規則,例如:

app.get("/notes", (req, res) => {
  // ...
});

這段程式就是在 Express 中建立一個 Route。實際開發時,URL、Endpoint、Route 這幾個名稱有時候會混著使用,所以不用過度糾結細微差異,先知道 URL 是實際網址、Endpoint 是 API 提供的入口,而 Route 是後端處理請求的規則即可。

為什麼需要 Router?

一開始直接把 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。

建立 Notes Router

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 層統一設定。

CommonJS、ES Module 與 Router 的匯入匯出

做到這裡會看到 module.exports = router,因為 Router 已經被拆到 routes/notes.jsapp.js 也需要取得這個 Router。不同檔案之間要互相使用程式碼,就需要透過模組系統來匯入與匯出內容。

Node.js 目前支援 CommonJS 與 ES Module 兩種主要的模組系統。CommonJS 使用 require() 引入模組,使用 module.exports 匯出內容;ES Module 則使用 importexport

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() 掛載 Router

Router 引入 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 /notesPOST /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,讓每個功能都有自己的位置。


上一篇
Day 08|API 不一定會成功:錯誤處理
下一篇
Day 10|Express 的中間人:Middleware 到底在做什麼?
系列文
出發吧!後端菜鳥:30 天的後端學習紀錄10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言