路由做的一件核心事
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/:id | req.params.id |
| Gin | /users/:id | c.Param("id") |
| Hono | /users/:id | c.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 名稱被框架固定(必須叫 index、show、store…)。
路由底層: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 在這個光譜的哪個位置。
