> ## Documentation Index
> Fetch the complete documentation index at: https://docs.devin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 按领域拆分单体 Express 路由

export const UseCaseHero = ({title, description, prompt, category, features, devinUrl, agent, intent, playbookId, type}) => {
  const encodedPrompt = encodeURIComponent(prompt || '');
  const tag = 'docs-use-case-gallery';
  const utm = 'utm_source=docs&utm_medium=use-case-gallery&utm_campaign=hero-cta';
  const agentParams = (agent ? '&agent=' + agent : '') + (intent ? '&intent=' + intent : '') + (playbookId ? '&playbookId=' + playbookId : '');
  const devinHref = type === 'schedule' ? 'https://app.devin.ai/settings/schedules/create?' + utm + agentParams + (prompt ? '&prompt=' + encodedPrompt : '') : type === 'review' ? 'https://app.devin.ai/review?' + utm : agent === 'ada' ? 'https://app.devin.ai/search?' + utm + '&noSubmit=true' + (prompt ? '&prompt=' + encodedPrompt : '') : devinUrl ? devinUrl.includes('?') ? devinUrl + '&' + utm + agentParams : devinUrl + '?' + utm + agentParams : prompt ? 'https://app.devin.ai/?tags=' + tag + '&' + utm + agentParams + '&prompt=' + encodedPrompt : 'https://app.devin.ai/?' + utm + agentParams;
  const buttonLabel = type === 'schedule' ? 'Schedule in Devin ↗' : type === 'review' ? 'Set Up Devin Review ↗' : agent === 'advanced' ? 'Try in Devin ↗' : agent === 'dana' ? 'Try in Dana ↗' : agent === 'ada' ? 'Try in Ask Devin ↗' : 'Try in Devin ↗';
  const featureList = features ? features.split(',').map(f => f.trim()) : [];
  return <div className="uc-hero">
      <div className="uc-hero-inner">
        <div className="uc-hero-left">
          <h1 className="uc-hero-title">{title}</h1>
          <p className="uc-hero-desc">{description}</p>
          <div>
            <a href={devinHref} target="_blank" rel="noopener noreferrer" className="try-in-devin-btn">
              {buttonLabel}
            </a>
          </div>
        </div>
        <div className="uc-hero-meta">
          <div className="uc-meta-item">
            <span className="uc-meta-label">Author</span>
            <span className="uc-meta-value">Cognition</span>
          </div>
          <div className="uc-meta-item">
            <span className="uc-meta-label">Category</span>
            <span className="uc-meta-value">{category}</span>
          </div>
          {featureList.length > 0 && <div className="uc-meta-item">
              <span className="uc-meta-label">Features</span>
              <span className="uc-meta-value">{featureList.join(', ')}</span>
            </div>}
        </div>
      </div>
    </div>;
};

export const PromptBlock = ({children, type, agent, intent, playbookId}) => {
  var utm = 'utm_source=docs&utm_medium=use-case-gallery&utm_campaign=prompt-block';
  var tag = 'docs-use-case-gallery';
  var agentParams = (agent ? '&agent=' + agent : '') + (intent ? '&intent=' + intent : '') + (playbookId ? '&playbookId=' + playbookId : '');
  var label = type === 'schedule' ? 'Schedule in Devin' : type === 'playbook' ? 'Create Playbook' : type === 'knowledge' ? 'Add to Knowledge' : agent === 'advanced' ? 'Try in Devin' : agent === 'dana' ? 'Try in Dana' : agent === 'ada' ? 'Try in Ask Devin' : 'Try in Devin';
  var buildUrl = function (text) {
    var encoded = encodeURIComponent(text);
    if (type === 'schedule') return 'https://app.devin.ai/settings/schedules/create?' + utm + agentParams + '&prompt=' + encoded;
    if (type === 'playbook') return 'https://app.devin.ai/settings/playbooks/create?' + utm + '&body=' + encoded;
    if (type === 'knowledge') return 'https://app.devin.ai/knowledge?' + utm + '&body=' + encoded;
    if (agent === 'ada') return 'https://app.devin.ai/search?' + utm + '&noSubmit=true&prompt=' + encoded;
    return 'https://app.devin.ai/?tags=' + tag + '&' + utm + agentParams + '&prompt=' + encoded;
  };
  const ref = React.useRef(null);
  const [href, setHref] = React.useState('#');
  React.useEffect(() => {
    if (!ref.current) return;
    var codeEl = ref.current.querySelector('pre code');
    if (codeEl) {
      var text = codeEl.textContent.trim();
      if (text) setHref(buildUrl(text));
    }
    var header = ref.current.querySelector('[data-component-part="code-block-header"]');
    if (header && !header.querySelector('.prompt-block-devin-link')) {
      var link = document.createElement('a');
      link.href = href;
      link.target = '_blank';
      link.rel = 'noopener noreferrer';
      link.className = 'prompt-block-devin-link';
      link.style.cssText = 'display:inline-flex;align-items:center;gap:6px;text-decoration:none;color:#fff;font-size:11px;font-weight:500;padding:4px 10px;border-radius:6px;white-space:nowrap;background:#317CFF;transition:background 0.2s;margin-left:8px;';
      link.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg> ' + label;
      link.onmouseenter = function () {
        link.style.background = '#2968D9';
      };
      link.onmouseleave = function () {
        link.style.background = '#317CFF';
      };
      header.appendChild(link);
    }
    var existingLink = ref.current.querySelector('.prompt-block-devin-link');
    if (existingLink && href !== '#') existingLink.href = href;
  });
  return <div className="prompt-block" ref={ref}>{children}</div>;
};

<UseCaseHero title="将单体 Express 路由拆分为领域路由" description="Devin 将一个 2,000 行的 Express 路由拆分为按领域划分的路由文件，并提取共享中间件 —— 然后更新所有 import 并确认所有测试全部通过。" prompt="重构 src/routes/index.ts —— 这是一个 2,000 行的单体 Express 路由，负责处理 users、orders、products 和 payments。将其拆分为位于 src/routes/ 下的按领域划分的路由文件，把共享中间件提取到 src/middleware/ 中，并在一个根路由中重新挂载所有路由。所有 112 个 API 测试仍必须全部通过。" category="代码质量" features="" />

<div className="uc-detail-wrapper">
  <Tip>不想手动配置？将本页链接粘贴到 Devin 的 session 中，让它帮你完成所有设置。</Tip>

  <Steps>
    <Step title="向 Devin 展示这个单体应用">
      你一定见过这样的文件——一个维护了一年半、越长越大的 Express 路由文件。每个业务域的每个接口都塞在 `src/routes/index.ts` 里：用户注册、支付 webhook、商品搜索全挤在一起。内联的鉴权检查被复制粘贴到了 40 个处理函数里。没人愿意动它，因为对订单逻辑的任何改动，都可能把上面三百行的用户相关接口改坏。

      这个文件顶部通常长这样：

      ```typescript src/routes/index.ts (before — 2,000 lines) theme={null}
      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 你希望目标结构是什么样的。

      <PromptBlock>
        ```txt 将单体路由器拆分为按领域划分的路由 theme={null}
        重构 src/routes/index.ts —— 这是一个 2,000 行的单体 Express
        路由器，在同一个文件中处理用户、订单、商品和支付。

        问题：
        - 认证中间件在每个处理函数中都被复制粘贴，而不是复用
        - 请求校验是内联的（没有 schema，只是零散的 if 判断）
        - 领域混杂：用户注册逻辑紧挨着支付 webhook
        - 路由定义与处理逻辑没有分离

        目标结构：
        - src/routes/users.ts       —— 所有 /users/* 端点
        - src/routes/products.ts    —— 所有 /products/* 端点
        - src/routes/orders.ts      —— 所有 /orders/* 端点
        - src/routes/payments.ts    —— 所有 /payments/* 端点
        - src/routes/index.ts       —— 挂载各领域路由的根路由器
        - src/middleware/auth.ts    —— 共享的 requireAuth 和 requireAdmin
        - src/middleware/validate.ts —— 共享的请求校验中间件

        要求：
        - 将认证中间件抽取到 src/middleware/auth.ts 中并在路由中引入
        - 按领域将处理函数分组到独立文件中
        - 保持所有路由路径完全一致（不能有破坏性 URL 变更）
        - tests/api/ 中现有的 112 个 API 测试必须在无需修改的情况下全部通过
        ```
      </PromptBlock>
    </Step>

    <Step title="用规范引导 Devin">
      Devin 会阅读你的 codebase 以推断出 patterns，但在重构场景中，[Knowledge](/zh/product-guides/knowledge) 条目能带来最大的价值。为 Devin 需要遵循的规范添加条目：

      * **Router patterns** — "每个业务域的 router 都使用 `Router()`，并在应用根部通过 `app.use('/domain', domainRouter)` 挂载"
      * **Middleware** — "认证中间件位于 `src/middleware/` 中，并且始终通过导入使用，绝不内联定义"
      * **Error handling** — "所有路由处理函数都使用来自 `src/lib/asyncHandler.ts` 的 `asyncHandler` 包装器——绝不直接使用 try/catch"

      让 Devin 参考你 codebase 中一个已经结构良好的 router，通常比从零开始用文字描述规范效果更好。在你的提示中加上一句类似 "遵循 `src/routes/admin.ts` 中的模式，该文件已经实现了清晰的结构分离"。

      你也可以让 Devin 为你生成 Knowledge 条目——只需描述你的规范，它就会创建结构良好的条目，供你审阅和保存。
    </Step>

    <Step title="审查 Devin 的拉取请求（PR）">
      Devin 会为每个端点建立映射，跟踪导入关系图，提取共享逻辑，创建领域文件，重新配置根路由，并运行你的测试套件。一个典型的 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.
      ```

      这是拆分后精简的根路由配置：

      ```typescript src/routes/index.ts (after — 15 lines) theme={null}
      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;
      ```

      以及一个已正确导入共享中间件的域路由文件：

      ```typescript src/routes/orders.ts (after — excerpt) theme={null}
      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` 处理，因此现有客户端和测试无需任何改动即可继续工作。
    </Step>

    <Step title="（可选）检出该分支并在本地测试">
      对于这样的结构性重构，建议先拉取该分支，在本地验证无误后再合并。你可以在 [Devin Desktop](https://windsurf.com) 或你常用的 IDE 中检出该分支，启动应用，并访问几个端点，确认路由、中间件和错误处理的行为与之前完全一致。

      ```bash theme={null}
      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 会看到并推送修复。
    </Step>

    <Step title="继续清理">
      路由拆分完成后，使用后续提示继续推进这次重构：

      <PromptBlock>
        ```txt 将内联校验提取为 schema theme={null}
        路由处理函数中仍然散落着用于输入校验的 if 语句。
        将这些校验提取为位于 src/schemas/ 目录下的 Zod schema —— 每个领域一个
        schema 文件（users.ts、orders.ts 等）。使用我们刚刚创建的
        validateBody 中间件。必须通过全部 112 个测试。
        ```
      </PromptBlock>

      <PromptBlock>
        ```txt 为新的路由文件添加集成测试 theme={null}
        现在每个领域的路由器都位于各自的文件中，但仍共用原有的测试套件。
        为 src/routes/orders.ts 编写有针对性的集成测试，覆盖创建、get 和退款——
        包括鉴权失败、校验错误，以及对已退款订单再次退款等边界情况。
        ```
      </PromptBlock>
    </Step>
  </Steps>
</div>
