Skip to main content

Einen monolithischen Express-Router in Domain-Routen aufteilen

Devin teilt einen 2.000-zeiligen Express-Router in domänenspezifische Routendateien mit gemeinsam genutzter Middleware auf – und aktualisiert anschließend alle Importe und stellt sicher, dass alle Tests erfolgreich sind.
AuthorCognition
CategoryCodequalität
1

Zeigen Sie Devin den Monolithen

Sie kennen die Datei – ein einzelner Express-Router, der über achtzehn Monate gewachsen ist. Jeder Endpoint für jede Domain lebt in src/routes/index.ts: Benutzerregistrierung neben Payment-Webhooks neben Produktsuche. Inline-Auth-Checks wurden in 40 Handlern per Copy & Paste dupliziert. Niemand will sie anfassen, weil eine Änderung an der Bestelllogik die Benutzer-Endpunkte dreihundert Zeilen weiter oben brechen könnte.So sieht der Anfang der Datei typischerweise aus:
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();

// ---- Auth-Middleware (überall hineinkopiert) ----
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" });
  }
};

// ---- Benutzer-Routen ----
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 */ });

// ---- Produkt-Routen ----
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 */ });

// ---- Bestell-Routen ----
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 */ });

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

// ... 1.400 weitere Zeilen mit gemischten Handlern, Inline-Validierung
//     und duplizierten Auth-Prüfungen

export default router;
Sag Devin ganz genau, wie die Zielstruktur aussehen soll.
2

Devin mit Konventionen anleiten

Devin liest deine Codebasis, um Muster zu erkennen, aber beim Refactoring sind Knowledge-Einträge am wertvollsten. Füge Einträge für Konventionen hinzu, denen Devin folgen soll:
  • Router-Muster — „Jeder Domain-Router verwendet Router() und wird in der Root-Datei mit app.use('/domain', domainRouter) eingebunden.“
  • Middleware — „Auth-Middleware befindet sich in src/middleware/ und wird immer importiert, niemals inline definiert.“
  • Fehlerbehandlung — „Alle Routen-Handler verwenden unseren asyncHandler-Wrapper aus src/lib/asyncHandler.ts — niemals ein rohes try/catch.“
Devin auf einen bereits gut strukturierten Router in deiner Codebasis zu verweisen, liefert oft bessere Ergebnisse, als Konventionen komplett neu zu beschreiben. Füge eine Zeile wie „Folge dem Muster in src/routes/admin.ts, das bereits sauber getrennt ist“ zu deinem Prompt hinzu.Du kannst auch Advanced Devin verwenden, um Knowledge-Einträge für dich zu erzeugen — beschreibe einfach deine Konventionen, und es erstellt gut strukturierte Einträge, die du überprüfen und speichern kannst.
3

PR von Devin überprüfen

Devin erfasst alle Endpoints, verfolgt den Importgraphen, extrahiert gemeinsam genutzte Logik, erstellt die Domain-Dateien, konfiguriert den Root-Router neu und führt Ihre Testsuite aus. So sieht der Pull Request (PR) typischerweise aus:
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.
So sieht der bereinigte Root Router nach der Aufteilung aus:
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;
Und eine Domain-Routing-Datei, in der die gemeinsame Middleware korrekt importiert ist:
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) => {
    // Rückerstattungslogik sauber aus dem Monolithen extrahiert
  }
);

export default router;
Jeder URL-Pfad bleibt identisch — /orders wird jetzt von ordersRouter verarbeitet, der unter /orders eingebunden ist, sodass bestehende Clients und Tests weiterhin ohne Änderungen funktionieren.
4

(Optional) Den Branch auschecken und lokal testen

Für einen strukturellen Refactor wie diesen lohnt es sich, den Branch zu pullen und die Änderungen lokal zu überprüfen, bevor du mergst. Schau ihn dir in Windsurf oder deiner bevorzugten IDE an, starte die App und rufe ein paar Endpoints auf, um sicherzustellen, dass sich Routing, Middleware und Fehlerbehandlung genauso verhalten wie zuvor.
git fetch origin && git checkout devin/refactor-router-split
npm install && npm test
npm run dev
# Einige Endpunkte aufrufen: curl http://localhost:3000/users, /orders, /payments
Falls dir etwas nicht richtig erscheint, hinterlasse einen Kommentar zum PR — Devin greift ihn auf und pusht einen Fix.
5

Bereinigung fortsetzen

Sobald der Router aufgeteilt ist, verwende Folge-Prompts, um das Refactoring zu erweitern: