09 — Router 层:路由配置

3 阅读3分钟

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 集中组装,也可以在各路由里分散组装
  • 路由级中间件用于给部分接口加特定功能(如鉴权、限流)