Skip to main content

将单体 Express 路由拆分为领域路由

Devin 将一个 2,000 行的 Express 路由拆分为按领域划分的路由文件,并提取共享中间件 —— 然后更新所有 import 并确认所有测试全部通过。
AuthorCognition
Category代码质量
1

向 Devin 展示这个单体应用

你一定见过这样的文件——一个维护了一年半、越长越大的 Express 路由文件。每个业务域的每个接口都塞在 src/routes/index.ts 里:用户注册、支付 webhook、商品搜索全挤在一起。内联的鉴权检查被复制粘贴到了 40 个处理函数里。没人愿意动它,因为对订单逻辑的任何改动,都可能把上面三百行的用户相关接口改坏。这个文件顶部通常长这样:
src/routes/index.ts (before — 2,000 lines)
import { Router } from "express";
import { db } from "../db";
import { stripe } from "../lib/stripe";
import { sendEmail } from "../lib/email";
import { logger } from "../lib/logger";

const router = Router();

// ---- 认证中间件(到处复制粘贴)----
const requireAuth = (req, res, next) => {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ error: "Unauthorized" });
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.status(401).json({ error: "Invalid token" });
  }
};

// ---- 用户路由 ----
router.post("/users/register", async (req, res) => { /* 45 lines */ });
router.post("/users/login", async (req, res) => { /* 30 lines */ });
router.get("/users/:id", requireAuth, async (req, res) => { /* 25 lines */ });
router.put("/users/:id", requireAuth, async (req, res) => { /* 40 lines */ });

// ---- 产品路由 ----
router.get("/products", async (req, res) => { /* 60 lines */ });
router.get("/products/:id", async (req, res) => { /* 35 lines */ });
router.post("/products", requireAuth, async (req, res) => { /* 50 lines */ });

// ---- 订单路由 ----
router.post("/orders", requireAuth, async (req, res) => { /* 80 lines */ });
router.get("/orders/:id", requireAuth, async (req, res) => { /* 40 lines */ });
router.post("/orders/:id/refund", requireAuth, async (req, res) => { /* 55 lines */ });

// ---- 支付路由 ----
router.post("/payments/webhook", async (req, res) => { /* 90 lines */ });
router.get("/payments/:id/status", requireAuth, async (req, res) => { /* 30 lines */ });

// ... 还有 1,400 行混合处理器、内联校验,
//     以及重复的认证检查

export default router;
明确告诉 Devin 你希望目标结构是什么样的。
2

通过约定引导 Devin

Devin 会阅读你的代码库以推断出模式,但在重构场景中,Knowledge 条目能带来最大的价值。为 Devin 需要遵循的约定添加条目:
  • Router patterns — “每个业务域的 router 都使用 Router(),并在应用根部通过 app.use('/domain', domainRouter) 挂载”
  • Middleware — “认证中间件位于 src/middleware/ 中,并且始终通过导入使用,绝不内联定义”
  • Error handling — “所有路由处理函数都使用来自 src/lib/asyncHandler.tsasyncHandler 包装器——绝不直接使用裸 try/catch”
让 Devin 参考你代码库中一个已经结构良好的 router,通常比从零开始用文字描述约定效果更好。在你的提示中加上一句类似 “遵循 src/routes/admin.ts 中的模式,该文件已经实现了清晰的结构分离”。你也可以使用 Advanced Devin 为你生成 Knowledge 条目——只需描述你的约定,它就会创建结构良好的条目,供你审阅和保存。
3

审查 Devin 的拉取请求(PR)

Devin 会为每个 endpoint 建立映射,跟踪导入关系图,提取共享逻辑,创建领域文件,重新配置根路由,并运行你的测试套件。一个典型的 PR 大致如下:
refactor: Split monolithic router into domain-specific route files

Files changed (8):
  src/routes/users.ts        — 4 endpoints, auth middleware imported
  src/routes/products.ts     — 3 endpoints, public + auth-protected
  src/routes/orders.ts       — 3 endpoints, all auth-protected
  src/routes/payments.ts     — 2 endpoints, webhook + status check
  src/routes/index.ts        — root router mounting all domains
  src/middleware/auth.ts      — requireAuth, requireAdmin (extracted)
  src/middleware/validate.ts  — validateBody schema middleware
  Old src/routes/index.ts     — 2,000-line monolith replaced

All 112 API tests pass. No URL changes.
这是拆分后精简的根路由配置:
src/routes/index.ts (after — 15 lines)
import { Router } from "express";
import usersRouter from "./users";
import productsRouter from "./products";
import ordersRouter from "./orders";
import paymentsRouter from "./payments";

const router = Router();

router.use("/users", usersRouter);
router.use("/products", productsRouter);
router.use("/orders", ordersRouter);
router.use("/payments", paymentsRouter);

export default router;
以及一个已正确导入共享中间件的域路由文件:
src/routes/orders.ts (after — excerpt)
import { Router } from "express";
import { requireAuth } from "../middleware/auth";
import { validateBody } from "../middleware/validate";
import { createOrderSchema, refundSchema } from "../schemas/orders";
import { db } from "../db";

const router = Router();

router.post("/", requireAuth, validateBody(createOrderSchema),
  async (req, res) => {
    const order = await db.orders.create({
      userId: req.user.id,
      items: req.body.items,
      total: req.body.total,
    });
    res.status(201).json(order);
  }
);

router.get("/:id", requireAuth, async (req, res) => {
  const order = await db.orders.findByPk(req.params.id);
  if (!order) return res.status(404).json({ error: "Order not found" });
  res.json(order);
});

router.post("/:id/refund", requireAuth, validateBody(refundSchema),
  async (req, res) => {
    // 退款逻辑已从单体应用中干净地提取出来
  }
);

export default router;
所有 URL 路径保持不变 —— /orders 现在由挂载在 /orders 下的 ordersRouter 处理,因此现有客户端和测试无需任何改动即可继续工作。
4

(可选)检出该分支并在本地测试

对于这样的结构性重构,建议先拉取该分支,在本地验证无误后再合并。你可以在 Windsurf 或你常用的 IDE 中打开项目,启动应用,并访问几个接口,确认路由、中间件和错误处理的行为与之前完全一致。
git fetch origin && git checkout devin/refactor-router-split
npm install && npm test
npm run dev
# 访问几个端点:curl http://localhost:3000/users, /orders, /payments
如果你发现有任何不对劲的地方,请在该 PR 上留下评论——Devin 会自动处理并推送修复。
5

继续清理

Once the router is split, use follow-up prompts to extend the refactoring: