9.2 基本用法
最简单的写法
import { Router } from 'express';
import { ProjectController } from '@/controllers/project/project-controller.js';
export function createProjectRouter(controller: ProjectController) {
const router = Router();
router.get('/projects', controller.list);
router.get('/projects/:id', controller.get);
router.post('/projects', controller.create);
router.put('/projects/:id', controller.update);
router.delete('/projects/:id', controller.remove);
return router;
}
用前缀分组
同一个模块的路由共享前缀:
export function createProjectRouter(controller: ProjectController) {
const router = Router();
// 所有路由自动带 /projects 前缀
router.get('/', controller.list); // GET /projects
router.get('/:id', controller.get); // GET /projects/:id
router.put('/', controller.create); // PUT /projects
router.put('/:id', controller.update); // PUT /projects/:id
router.delete('/:id', controller.remove); // DELETE /projects/:id
return router;
}
然后在父路由里挂上前缀:
router.use('/projects', createProjectRouter(projectController));
9.3 路由的层级组织
按版本 + 按模块
大型项目推荐这样组织:
src/routers/
├── index.ts # 根路由,挂载所有子路由
├── api-router.ts # /api 总路由
├── v1/
│ └── index.ts # /api/v1 版本路由
├── v2/
│ └── index.ts # /api/v2 版本路由
├── project/
│ └── project-router.ts # 项目管理模块路由
└── user/
└── user-router.ts # 用户管理模块路由
根路由:src/routers/index.ts
import { Router } from 'express';
import { createApiRouter } from './api-router.js';
export function createRouter() {
const router = Router();
// API 路由
router.use('/api', createApiRouter());
// 健康检查(不放在 /api 下也行)
router.get('/health', (_req, res) => {
res.json({ status: 'ok' });
});
return router;
}
API 总路由:src/routers/api-router.ts
import { Router } from 'express';
import { createV1Router } from './v1/index.js';
export function createApiRouter() {
const router = Router();
router.use('/v1', createV1Router());
// router.use('/v2', createV2Router()); // 以后加 v2 版本
return router;
}
版本路由:src/routers/v1/index.ts
import { Router } from 'express';
import type { AppModels } from '@/bootstrap.js';
import { ProjectController } from '@/controllers/project/project-controller.js';
import { UserController } from '@/controllers/user/user-controller.js';
import { ProjectService } from '@/services/project/project-service.js';
import { UserService } from '@/services/user/user-service.js';
import { createProjectRouter } from '../project/project-router.js';
import { createUserRouter } from '../user/user-router.js';
export function createV1Router(models: AppModels) {
const router = Router();
// 组装各模块的 Controller 和 Service
const projectController = new ProjectController(
new ProjectService(models.projects),
);
const userController = new UserController(
new UserService(models.users),
);
// 挂载各模块路由
router.use('/projects', createProjectRouter(projectController));
router.use('/users', createUserRouter(userController));
return router;
}
模块路由:src/routers/project/project-router.ts
import { Router } from 'express';
import type { ProjectController } from '@/controllers/project/project-controller.js';
export function createProjectRouter(controller: ProjectController) {
const router = Router();
router.get('/', controller.list);
router.get('/:id', controller.get);
router.put('/', controller.create);
router.put('/:id', controller.update);
router.delete('/:id', controller.remove);
return router;
}
9.4 为什么在版本路由里组装 Controller
你可能注意到了:Controller 和 Service 的组装(new)放在了版本路由里,而不是 bootstrap.ts。
两种做法都可以:
| 做法 | 优点 | 缺点 |
|---|---|---|
| bootstrap 里组装全部 | 统一管理,所有依赖一目了然 | bootstrap 文件会越来越长 |
| 各版本路由里组装 | 每个版本自己决定用哪些模块,不同版本可以有不同实现 | 依赖分散在多处 |
中小项目推荐放 bootstrap 统一管理,大项目可以按模块/按版本分散。
本教程为了简单清晰,采用集中在 bootstrap 组装的方式(见第 10 篇)。
9.5 路由级中间件
除了全局中间件,还可以给某个路由或某组路由单独加中间件。
给单个路由加
router.get('/projects', authMiddleware, controller.list);
给一组路由加
const router = Router();
// 这些不需要登录
router.post('/login', authController.login);
router.post('/register', authController.register);
// 下面的都需要登录
router.use(authMiddleware);
router.get('/projects', controller.list);
router.get('/projects/:id', controller.get);
给子路由加
// 管理后台的路由都需要管理员权限
router.use('/admin', requireAdmin, createAdminRouter(adminController));
路由级中间件的执行顺序是:先执行全局的,再依次执行路由上挂载的。
9.6 路由参数的类型
req.params 里的参数都是字符串,哪怕你定义的是数字 ID。
router.get('/projects/:id', (req, res) => {
console.log(typeof req.params.id); // "string"
// 用的时候要转成数字
const id = Number(req.params.id);
});
这也是为什么我们在 Controller 里用 Number(req.params.id) 转一下。
9.7 常见的路由组织问题
❌ 把业务逻辑写在路由文件里
// 反面例子
router.get('/projects', async (req, res) => {
// 写了一堆业务逻辑
const db = await getDB();
const list = await db.query('SELECT * FROM projects');
res.json({ status: 0, data: list });
});
路由文件只负责 URL 映射,业务逻辑放在 Controller 和 Service。
❌ 路由层级太深
/api/v1/admin/settings/notification/email/template/list
层级太多不便于理解和查找。一般控制在 3-4 层以内比较合适。
❌ 同一个接口有多种写法
// 一会用 get,一会用 post,参数位置也不统一
router.get('/project/list', ...);
router.post('/project/add', ...);
router.get('/project/detail/:id', ...);
保持风格一致,要么都 RESTful,要么都用动词后缀。
9.8 小结
- Router 层负责 URL 到 Controller 的映射
- 按版本 + 按模块组织路由,结构清晰
- 路由文件只写映射关系,不写业务逻辑
- Controller 可以在 bootstrap 集中组装,也可以在各路由里分散组装
- 路由级中间件用于给部分接口加特定功能(如鉴权、限流)