Skip to main content

Dividir un router monolítico de Express en rutas por dominio

Devin divide un router de Express de 2.000 líneas en archivos de rutas específicos por dominio con middleware compartido, luego actualiza cada import y verifica que todas las pruebas sigan pasando.
AuthorCognition
CategoryCalidad de código
1

Muéstrale el monolito a Devin

Conoces ese archivo: un único router de Express que ha ido creciendo durante dieciocho meses. Todos los endpoints de cada dominio viven en src/routes/index.ts: el registro de usuarios junto a los webhooks de pagos junto a la búsqueda de productos. Las comprobaciones de autenticación incrustadas en el código están copiadas y pegadas en 40 controladores. Nadie quiere tocarlo porque un cambio en la lógica de pedidos podría romper los endpoints de usuario trescientas líneas más arriba.Así es como suele verse la parte superior del archivo:
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 de autenticación (copiado y pegado en todos lados) ----
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" });
  }
};

// ---- Rutas de usuarios ----
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 */ });

// ---- Rutas de productos ----
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 */ });

// ---- Rutas de pedidos ----
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 */ });

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

// ... 1.400 líneas más de handlers mezclados, validación en línea,
//     y verificaciones de autenticación duplicadas

export default router;
Dile a Devin exactamente cómo quieres que se vea la estructura objetivo.
2

Guía a Devin mediante convenciones

Devin lee tu base de código para inferir patrones, pero la refactorización es donde las entradas de Knowledge aportan más valor. Agrega entradas para las convenciones que Devin debe seguir:
  • Patrones de router — “Cada router de dominio usa Router() y se monta con app.use('/domain', domainRouter) en la raíz”
  • Middleware — “El middleware de autenticación vive en src/middleware/ y siempre se importa, nunca se define en línea”
  • Manejo de errores — “Todos los manejadores de rutas usan nuestro wrapper asyncHandler de src/lib/asyncHandler.ts — nunca un try/catch directo”
Indicarle a Devin un router ya bien estructurado en tu base de código suele producir mejores resultados que describir las convenciones desde cero. Agrega una línea como “Sigue el patrón en src/routes/admin.ts, que ya está claramente separado” a tu prompt.También puedes usar Advanced Devin para generar entradas de Knowledge para ti: solo describe tus convenciones y creará entradas bien estructuradas que podrás revisar y guardar.
3

Revisar el PR de Devin

Devin mapea todos los endpoints, sigue el grafo de imports, extrae la lógica compartida, crea los archivos de dominio, reconfigura el router raíz y ejecuta tu suite de pruebas. Así es como suele verse el 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.
Así queda el router raíz limpio después de la división:
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;
Y un archivo de rutas de dominio donde se importa correctamente el middleware compartido:
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) => {
    // lógica de reembolso extraída limpiamente del monolito
  }
);

export default router;
Cada ruta de URL permanece igual: /orders ahora es gestionada por ordersRouter, montado en /orders, por lo que los clientes y las pruebas existentes funcionan sin necesidad de cambios.
4

(Opcional) Cambia a la rama y prueba localmente

Para una refactorización estructural como esta, vale la pena hacer checkout de la rama y verificarla localmente antes de fusionarla. Pruébala en Windsurf o en tu IDE preferido, levanta la aplicación y prueba algunos endpoints para confirmar que el enrutamiento, el middleware y el manejo de errores se comportan igual que antes.
git fetch origin && git checkout devin/refactor-router-split
npm install && npm test
npm run dev
# Prueba algunos endpoints: curl http://localhost:3000/users, /orders, /payments
Si ves algo raro, deja un comentario en el PR: Devin lo detectará y subirá una corrección.
5

Continuar con la limpieza

Una vez que el router esté dividido, usa prompts de seguimiento para ampliar el refactor: