> ## 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.

# Split a Monolithic Express Router into Domain Routes

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="Split a Monolithic Express Router into Domain Routes" description="Devin breaks a 2,000-line Express router into domain-specific route files with shared middleware — then updates every import and verifies all tests pass." prompt="Refactor src/routes/index.ts — it's a 2,000-line monolithic Express router handling users, orders, products, and payments. Split it into domain-specific route files under src/routes/, extract shared middleware into src/middleware/, and re-mount everything in a root router. All 112 API tests must still pass." category="Code Quality" features="" />

<div className="uc-detail-wrapper">
  <Tip>Don't want to set this up manually? Paste a link to this page into a Devin session and ask it to set everything up for you.</Tip>

  <Steps>
    <Step title="Show Devin the monolith">
      You know the file — one Express router that grew for eighteen months. Every endpoint for every domain lives in `src/routes/index.ts`: user registration next to payment webhooks next to product search. Inline auth checks are copy-pasted across 40 handlers. Nobody wants to touch it because a change to the orders logic might break the user endpoints three hundred lines above.

      Here's what the top of the file typically looks like:

      ```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();

      // ---- Auth middleware (copy-pasted everywhere) ----
      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" });
        }
      };

      // ---- User routes ----
      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 */ });

      // ---- Product routes ----
      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 */ });

      // ---- Order routes ----
      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 */ });

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

      // ... 1,400 more lines of mixed handlers, inline validation,
      //     and duplicated auth checks

      export default router;
      ```

      Tell Devin exactly what you want the target structure to look like.

      <PromptBlock>
        ```txt Split monolithic router into domain routes theme={null}
        Refactor src/routes/index.ts — it's a 2,000-line monolithic Express
        router handling users, orders, products, and payments all in one file.

        Problems:
        - Auth middleware is copy-pasted in every handler instead of shared
        - Inline request validation (no schema, just scattered if-statements)
        - Mixed domains: user registration next to payment webhooks
        - No separation of route definitions from handler logic

        Target structure:
        - src/routes/users.ts      — all /users/* endpoints
        - src/routes/products.ts   — all /products/* endpoints
        - src/routes/orders.ts     — all /orders/* endpoints
        - src/routes/payments.ts   — all /payments/* endpoints
        - src/routes/index.ts      — root router that mounts each domain
        - src/middleware/auth.ts    — shared requireAuth and requireAdmin
        - src/middleware/validate.ts — shared request validation middleware

        Requirements:
        - Extract auth middleware into src/middleware/auth.ts and import it
        - Group handlers by domain into separate files
        - Keep all route paths identical (no breaking URL changes)
        - All 112 existing API tests in tests/api/ must pass without changes
        ```
      </PromptBlock>
    </Step>

    <Step title="Guide Devin with conventions">
      Devin reads your codebase to infer patterns, but refactoring is where [Knowledge](/product-guides/knowledge) entries pay off the most. Add entries for conventions Devin should follow:

      * **Router patterns** — "Each domain router uses `Router()` and is mounted with `app.use('/domain', domainRouter)` in the root"
      * **Middleware** — "Auth middleware lives in `src/middleware/` and is always imported, never defined inline"
      * **Error handling** — "All route handlers use our `asyncHandler` wrapper from `src/lib/asyncHandler.ts` — never raw try/catch"

      Pointing Devin at an already well-structured router in your codebase often produces better results than describing conventions from scratch. Add a line like "Follow the pattern in `src/routes/admin.ts`, which is already cleanly separated" to your prompt.

      You can also ask Devin to generate Knowledge entries for you — just describe your conventions and it will create well-structured entries you can review and save.
    </Step>

    <Step title="Review Devin's PR">
      Devin maps every endpoint, traces the import graph, extracts shared logic, creates the domain files, rewires the root router, and runs your test suite. Here's what the PR typically looks like:

      ```
      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.
      ```

      Here's what the clean root router looks like after the split:

      ```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;
      ```

      And a domain route file with the shared middleware properly imported:

      ```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) => {
          // refund logic extracted cleanly from the monolith
        }
      );

      export default router;
      ```

      Every URL path stays identical — `/orders` is now handled by `ordersRouter` mounted at `/orders`, so existing clients and tests work without changes.
    </Step>

    <Step title="(Optional) Check out the branch and test locally">
      For a structural refactor like this, it's worth pulling the branch and verifying locally before merging. Check it out in [Devin Desktop](https://windsurf.com) or your preferred IDE, boot the app, and hit a few endpoints to confirm routing, middleware, and error handling all behave the same as before.

      ```bash theme={null}
      git fetch origin && git checkout devin/refactor-router-split
      npm install && npm test
      npm run dev
      # Hit a few endpoints: curl http://localhost:3000/users, /orders, /payments
      ```

      If anything looks off, leave a comment on the PR — Devin will pick it up and push a fix.
    </Step>

    <Step title="Continue the cleanup">
      Once the router is split, use follow-up prompts to extend the refactoring:

      <PromptBlock>
        ```txt Extract inline validation into schemas theme={null}
        The route handlers still have scattered if-statements for input
        validation. Extract them into Zod schemas in src/schemas/ — one
        schema file per domain (users.ts, orders.ts, etc.). Use the
        validateBody middleware we just created. All 112 tests must pass.
        ```
      </PromptBlock>

      <PromptBlock>
        ```txt Add integration tests for new route files theme={null}
        Each domain router now lives in its own file but shares the original
        test suite. Write focused integration tests for src/routes/orders.ts
        covering create, get, and refund — including auth failures, validation
        errors, and edge cases like refunding an already-refunded order.
        ```
      </PromptBlock>
    </Step>
  </Steps>
</div>
