Skip to main content

Dividir um roteador Express monolítico em rotas por domínio

Devin divide um roteador Express de 2.000 linhas em arquivos de rota específicos de domínio com middleware compartilhado — depois atualiza todos os imports e verifica se todos os testes continuam passando.
AuthorCognition
CategoryQualidade do Código
1

Mostre o monólito para o Devin

Você conhece aquele arquivo — um único router do Express que foi crescendo por dezoito meses. Todo endpoint de todo domínio fica em src/routes/index.ts: cadastro de usuário ao lado de webhooks de pagamento ao lado de busca de produto. Verificações de autenticação inline são copiadas e coladas em 40 handlers. Ninguém quer mexer nele porque uma alteração na lógica de pedidos pode quebrar os endpoints de usuário trezentas linhas acima.É mais ou menos assim que o topo do arquivo costuma ser:
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 autenticação (copiado em todo lugar) ----
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" });
  }
};

// ---- Rotas de usuário ----
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 */ });

// ---- Rotas de produto ----
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 */ });

// ---- Rotas de pedido ----
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 */ });

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

// ... mais 1.400 linhas de handlers misturados, validação inline,
//     e verificações de autenticação duplicadas

export default router;
Diga ao Devin exatamente como você quer que a estrutura desejada fique.
2

Oriente o Devin com convenções

Devin lê sua codebase para inferir padrões, mas é na refatoração que as entradas de Knowledge geram mais impacto. Adicione entradas com as convenções que Devin deve seguir:
  • Padrões de router — “Cada router de domínio usa Router() e é montado com app.use('/domain', domainRouter) na raiz”
  • Middleware — “O middleware de autenticação fica em src/middleware/ e é sempre importado, nunca definido inline”
  • Tratamento de erros — “Todos os route handlers usam nosso wrapper asyncHandler de src/lib/asyncHandler.ts — nunca try/catch direto”
Direcionar o Devin a um router já bem estruturado na sua codebase muitas vezes produz resultados melhores do que descrever convenções do zero. Adicione uma linha como “Siga o padrão em src/routes/admin.ts, que já está claramente separado” ao seu prompt.Você também pode usar o Advanced Devin para gerar entradas de Knowledge para você — basta descrever suas convenções e ele criará entradas bem estruturadas que você pode revisar e salvar.
3

Analise o PR do Devin

Devin mapeia todos os endpoints, rastreia o grafo de importações, extrai a lógica compartilhada, cria os arquivos de domínio, reconfigura o roteador raiz e executa sua suíte de testes. É assim que o PR geralmente fica:
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.
Veja como fica o roteador raiz limpo após a separação:
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 um arquivo de rota de domínio em que o middleware compartilhado é importado corretamente:
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 de forma limpa do monólito
  }
);

export default router;
Cada caminho de URL permanece o mesmo — /orders agora é tratado pelo ordersRouter montado em /orders, de forma que os clientes e testes existentes continuam funcionando sem nenhuma alteração.
4

(Opcional) Faça o checkout da branch e teste localmente

Para uma refatoração estrutural como esta, vale a pena puxar a branch e verificar localmente antes de fazer o merge. Abra no Windsurf ou na sua IDE preferida, inicie o app e acesse alguns endpoints para confirmar que o roteamento, o middleware e o tratamento de erros continuam se comportando exatamente como antes.
git fetch origin && git checkout devin/refactor-router-split
npm install && npm test
npm run dev
# Teste alguns endpoints: curl http://localhost:3000/users, /orders, /payments
Se notar algo estranho, deixe um comentário no PR — Devin vai detectar e aplicar uma correção.
5

Continuar limpeza

Depois que o router for dividido, use prompts de acompanhamento para ampliar a refatoração: