Skip to main content

Diviser un routeur Express monolithique en routes par domaine

Devin découpe un routeur Express de 2 000 lignes en fichiers de routes spécifiques à chaque domaine avec un middleware partagé — puis met à jour tous les imports et vérifie que tous les tests passent.
AuthorCognition
CategoryQualité du code
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 utiliser Advanced Devin pour 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 changement.
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 le prendra en compte et poussera un correctif.
5

Continuer le nettoyage

Once the router is split, use follow-up prompts to extend the refactoring: