Skip to main content

Suddividere un router Express monolitico in route di dominio

Devin suddivide un router Express di 2.000 righe in file di route specifici per dominio con middleware condiviso — quindi aggiorna tutti gli import e verifica che tutti i test vengano superati.
AuthorCognition
CategoryQualità del codice
1

Mostra il monolite a Devin

Conosci il file — un unico router Express cresciuto per diciotto mesi. Ogni endpoint per ogni dominio vive in src/routes/index.ts: la registrazione utente accanto ai webhook di pagamento accanto alla ricerca dei prodotti. I controlli di autenticazione inline sono copiati e incollati in 40 handler diversi. Nessuno vuole toccarlo perché una modifica alla logica degli ordini potrebbe compromettere gli endpoint utente trecento righe più sopra.Ecco come di solito si presenta l’inizio del file:
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();

// ---- Middleware di autenticazione (copiato ovunque) ----
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" });
  }
};

// ---- Route utenti ----
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 */ });

// ---- Route prodotti ----
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 */ });

// ---- Route ordini ----
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 */ });

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

// ... altre 1.400 righe di handler misti, validazione inline,
//     e controlli di autenticazione duplicati

export default router;
Dì a Devin esattamente come vuoi che sia la struttura finale.
2

Guida Devin con le convenzioni

Devin legge la tua codebase per individuare i pattern, ma è nel refactoring che le voci in Knowledge sono più utili. Aggiungi voci per le convenzioni che Devin dovrebbe seguire:
  • Router patterns — “Ogni domain router utilizza Router() ed è montato con app.use('/domain', domainRouter) nella root”
  • Middleware — “Il middleware di autenticazione si trova in src/middleware/ ed è sempre importato, mai definito inline”
  • Error handling — “Tutti i route handler usano il nostro wrapper asyncHandler da src/lib/asyncHandler.ts — mai un try/catch diretto”
Far lavorare Devin su un router già ben strutturato nella tua codebase spesso produce risultati migliori rispetto a descrivere le convenzioni da zero. Aggiungi una riga come “Segui il pattern in src/routes/admin.ts, che è già ben separato” al tuo prompt.Puoi anche usare Advanced Devin per generare voci di Knowledge per te — ti basta descrivere le tue convenzioni e creerà voci ben strutturate che puoi rivedere e salvare.
3

Rivedi la PR di Devin

Devin mappa ogni endpoint, traccia il grafo degli import, estrae la logica condivisa, crea i file di dominio, riconfigura il router root ed esegue la tua suite di test. Ecco come si presenta in genere una 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.
Ecco come si presenta il router root pulito dopo la suddivisione:
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;
E un file di route per il dominio, con il middleware condiviso correttamente importato:
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) => {
    // logica di rimborso estratta in modo pulito dal monolite
  }
);

export default router;
Ogni percorso URL rimane invariato — /orders è ora gestito da ordersRouter montato su /orders, quindi i client e i test esistenti continuano a funzionare senza modifiche.
4

(Facoltativo) Esegui il checkout del branch e testa in locale

Per un refactoring strutturale di questo tipo, vale la pena fare pull del branch e verificare in locale prima di effettuare il merge. Aprilo in Windsurf o nel tuo IDE preferito, avvia l’app e prova alcuni endpoint per confermare che routing, middleware e gestione degli errori si comportino esattamente come prima.
git fetch origin && git checkout devin/refactor-router-split
npm install && npm test
npm run dev
# Testa alcuni endpoint: curl http://localhost:3000/users, /orders, /payments
Se qualcosa non ti convince, lascia un commento sulla PR — Devin lo prenderà in carico e apporterà una correzione.
5

Prosegui la pulizia

Una volta che il router è stato suddiviso, utilizza prompt successivi per ampliare il refactoring: