Skip to main content
1

Présentez le monolithe à Devin

Vous connaissez ce fichier : un seul routeur Express qui a grossi pendant dix-huit mois. Tous les endpoints pour tous les domaines se trouvent dans src/routes/index.ts : l’inscription utilisateur à côté des webhooks de paiement, à côté de la recherche de produits. Les vérifications d’authentification en ligne sont copiées-collées dans 40 gestionnaires. Personne ne veut y toucher, car une modification de la logique des commandes pourrait casser les endpoints utilisateur trois cents lignes plus haut dans le fichier.Voici à quoi ressemble généralement le haut du fichier :
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 d'authentification (copié-collé partout) ----
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" });
  }
};

// ---- Routes utilisateurs ----
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 */ });

// ---- Routes produits ----
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 */ });

// ---- Routes commandes ----
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 */ });

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

// ... 1 400 lignes supplémentaires de gestionnaires mélangés, validation inline,
//     et vérifications d'authentification dupliquées

export default router;
Dites à Devin exactement à quoi doit ressembler la structure cible.
2

Guidez Devin à l’aide de conventions

Devin lit votre base de code pour en déduire des patterns, mais c’est lors de la refactorisation que les entrées Knowledge sont les plus utiles. Ajoutez des entrées pour les conventions que Devin doit suivre :
  • Patterns de routeur — “Chaque routeur de domaine utilise Router() et est monté avec app.use('/domain', domainRouter) à la racine”
  • Middleware — “Le middleware d’authentification se trouve dans src/middleware/ et est toujours importé, jamais défini inline”
  • Gestion des erreurs — “Tous les gestionnaires de route utilisent notre wrapper asyncHandler depuis src/lib/asyncHandler.ts — jamais de try/catch direct”
Orienter Devin vers un routeur déjà bien structuré dans votre base de code produit souvent de meilleurs résultats que de décrire les conventions à partir de zéro. Ajoutez une ligne comme “Suivez le pattern dans src/routes/admin.ts, qui est déjà clairement séparé” à votre prompt.Vous pouvez également demander à Devin de générer des entrées Knowledge pour vous — décrivez simplement vos conventions et il créera des entrées bien structurées que vous pourrez relire et enregistrer.
3

Examiner la PR de Devin

Devin cartographie tous les endpoints, suit le graphe d’import, extrait la logique partagée, crée les fichiers de domaine, rebranche le routeur racine et exécute votre suite de tests. Voici à quoi ressemble généralement la 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.
Voici à quoi ressemble le routeur racine simplifié après la séparation :
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;
Et un fichier de routes de domaine où le middleware partagé est correctement importé :
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) => {
    // logique de remboursement extraite proprement du monolithe
  }
);

export default router;
Chaque chemin d’URL reste identique — /orders est désormais géré par ordersRouter, monté sur /orders, de sorte que les clients et les tests existants continuent de fonctionner sans modification.
4

(Facultatif) Basculez sur la branche et testez en local

Pour une refactorisation structurelle de ce type, il est préférable de récupérer la branche et de vérifier en local avant de fusionner. Ouvrez le projet dans Windsurf ou dans votre IDE préféré, lancez l’application et appelez quelques endpoints pour confirmer que le routage, le middleware et la gestion des erreurs se comportent comme auparavant.
git fetch origin && git checkout devin/refactor-router-split
npm install && npm test
npm run dev
# Testez quelques endpoints : curl http://localhost:3000/users, /orders, /payments
Si quelque chose vous semble incorrect, laissez un commentaire sur la PR — Devin en tiendra compte et poussera un correctif.
5

Continuer le nettoyage

Une fois le routeur séparé, utilisez des prompts de suivi pour prolonger la refactorisation :