路由做的一件核心事

HTTP 請求進來帶兩個關鍵資訊:method(GET / POST / PUT / DELETE)和 URL path/users/42)。

路由器的工作是:把這個 method + path 的組合,對應到一個 handler function。

這個對應,各框架做法差很多。


宣告式路由(Express / Gin / Hono)

最直觀的寫法——你直接宣告「這個 URL 交給這個 handler」:

// Express
const router = express.Router();
 
router.get('/users', UserController.index);
router.get('/users/:id', UserController.show);
router.post('/users', validate(userSchema), UserController.store);
router.put('/users/:id', authenticate, UserController.update);
router.delete('/users/:id', authenticate, UserController.destroy);
// Gin
r := gin.New()
r.GET("/users", controllers.Index)
r.GET("/users/:id", controllers.Show)
r.POST("/users", middleware.Validate(schemas.User), controllers.Store)

優點:一眼就看到全部的路由,不需要猜。
缺點:routes 很多時,路由檔案會很長。解法是拆成多個 router 再 mount。

動態參數的語法各框架不一樣:

框架語法取值方式
Express/users/:idreq.params.id
Gin/users/:idc.Param("id")
Hono/users/:idc.req.param('id')
FastAPI/users/{id}function parameter
Spring/users/{id}@PathVariable

Decorator 路由(NestJS / Spring Boot / FastAPI)

Decorator / annotation 把路由定義直接放在 handler 旁邊:

// NestJS
@Controller('users')
export class UserController {
  @Get()
  index() { return this.userService.findAll(); }
 
  @Get(':id')
  show(@Param('id') id: string) { return this.userService.findOne(+id); }
 
  @Post()
  @UseGuards(AuthGuard)
  store(@Body() dto: CreateUserDto) { return this.userService.create(dto); }
}
# FastAPI
@app.get("/users")
def get_users(): ...
 
@app.get("/users/{id}")
def get_user(id: int): ...
 
@app.post("/users", status_code=201)
def create_user(user: UserCreateSchema): ...
// Spring Boot
@RestController
@RequestMapping("/users")
public class UserController {
  @GetMapping
  public List<User> index() { ... }
 
  @GetMapping("/{id}")
  public User show(@PathVariable Long id) { ... }
 
  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  public User store(@RequestBody @Valid UserCreateDto dto) { ... }
}

優點:路由定義和 handler 邏輯放在一起,看 controller 就知道這個 class 管哪些 endpoint。
缺點:要看全部路由需要翻多個 controller 檔案,或靠 IDE 的 navigation。沒有一個地方可以「一眼看到所有路由」。


Convention 路由(Rails / Laravel)

最 magic 的做法——根據 class 名稱和 method 名稱自動推斷路由:

# Rails config/routes.rb
Rails.application.routes.draw do
  resources :users  # 自動生成 7 個 RESTful routes
end
 
# 對應:
# GET    /users          → UsersController#index
# GET    /users/new      → UsersController#new
# POST   /users          → UsersController#create
# GET    /users/:id      → UsersController#show
# GET    /users/:id/edit → UsersController#edit
# PATCH  /users/:id      → UsersController#update
# DELETE /users/:id      → UsersController#destroy
// Laravel
Route::apiResource('users', UserController::class);
// 同樣自動生成 RESTful 路由對應到 controller method

優點:標準 CRUD 幾乎不用寫,resources :users 一行搞定。
缺點:非標準行為(例如 GET /users/export)要打破 convention,而且 controller method 名稱被框架固定(必須叫 indexshowstore…)。


路由底層:Trie vs 線性搜尋

路由器不是 if-else 列表。路由數量一多,線性搜尋就慢了。主流框架用的是 Radix Tree(壓縮 Trie)

/
├── users/
│   ├── :id          → UserController.show
│   ├── :id/posts    → PostController.byUser
│   └── export       → UserController.export
└── posts/
    └── :id          → PostController.show

Radix Tree 讓路由查找的時間複雜度接近 O(k),其中 k 是 URL 段數,而不是路由總數。這是 Gin / Hono / Fastify 在 benchmark 上效能明顯優於 Express 的原因之一——Express 用的是較簡單的 layer list。


Route Group 與 Middleware 作用域

路由的另一個重要功能:讓 middleware 只作用在特定路由上。

// Express:router-level middleware
const apiRouter = express.Router();
apiRouter.use(authenticate); // 只在這個 router 的所有 routes 跑
 
apiRouter.get('/users', UserController.index);
apiRouter.post('/users', UserController.store);
 
app.use('/api/v1', apiRouter);
// GET /api/v1/users → authenticate → UserController.index
// GET /           → 不跑 authenticate(不在 apiRouter scope 內)
// NestJS:Guard 作用在 Controller 或 Method 層
@UseGuards(AuthGuard)
@Controller('users')
class UserController {
  @Get()
  index() { ... } // ← AuthGuard 作用
 
  @Get('public')
  @SkipAuth()     // ← 例外不跑 Guard
  publicInfo() { ... }
}

這個「middleware 作用域」的設計在各框架的實作不同,但概念是一樣的:你要能清楚定義「哪些 route 跑哪些 middleware」,不然所有 route 都跑所有 middleware 是不對的。


在 Express 子系列的應用

Express 的路由系統詳細實作見 [[backend/framework/express/router-module|[express][M6] Router 拆分與 Module 化]]。Express 選擇宣告式路由 + express.Router() 組合,這裡的概念對照著看,你能清楚理解 Express 在這個光譜的哪個位置。