Automatiser un refactoring sûr avec Claude Code
Workflow concret pour refactorer avec Claude Code: exemples avant/après, tests, git diff, revue, risques et prompts prêts à l'emploi.
Commencer par poser une limite de sécurité
La meilleure manière d’automatiser le refactoring avec Claude Code n’est pas de demander “nettoie tout le projet”. Cette demande paraît efficace, mais elle produit souvent un diff trop grand, difficile à relire, avec des changements de comportement cachés. Refactorer signifie améliorer la structure interne du code sans changer ce que voient les utilisateurs, les APIs, les tests et les systèmes qui consomment ce code.
Dans ce guide, Claude Code est utilisé comme un partenaire de travail: il inspecte le dépôt, propose un petit plan, modifie une seule zone, exécute les commandes de vérification, puis explique le git diff. Les common workflows officiels donnent une bonne base. Pour les permissions de commandes et les réglages de projet, consulte aussi la documentation settings.
Si ton équipe formalise encore ses règles, lis aussi les articles sur les permissions Claude Code et la gestion du contexte Claude Code. Ici, on transforme ces principes en routine: quoi demander en premier, quels changements sont sûrs pour débuter, comment tester et comment relire.
Note de terrain de Masa: dans des essais courts, Claude Code a été très efficace pour renommer, extraire des fonctions pures, clarifier des types TypeScript et ajouter des tests de régression. Il est devenu beaucoup plus difficile à contrôler avec des prompts vagues comme “modernise ce service”. Le workflow ennuyeux est le plus rentable: périmètre étroit, tests visibles, diff lisible.
Workflow sûr: inspecter, planifier, changer une chose, vérifier
Garde cet ordre jusqu’à ce que l’équipe ait confiance dans le processus.
| Étape | Ce que fait Claude Code | Ce que vérifie l’humain |
|---|---|---|
| 1. Inspecter | Lit les fichiers, dépendances et tests | Le périmètre reste raisonnable |
| 2. Planifier | Propose un plan de trois étapes maximum | Aucun changement fonctionnel n’est caché |
| 3. Modifier | Change un seul thème | Le diff peut être relu |
| 4. Vérifier | Lance test, typecheck et lint | Les échecs sont expliqués |
| 5. Relire | Résume git diff et les risques | Le comportement avant/après reste équivalent |
Commence par un prompt sans édition.
Inspecte ce dépôt pour trouver des candidats sûrs de refactoring.
Ne modifie aucun fichier pour l'instant.
Contraintes:
- Ne pas changer le comportement externe
- Limiter un diff à trois fichiers ou moins
- Prioriser les zones couvertes par des tests
- Retourner un tableau: candidat, raison, commande de vérification, risque
“Ne modifie aucun fichier” est une phrase importante. Claude Code peut passer rapidement de l’analyse à l’action. Séparer diagnostic et implémentation réduit fortement le risque.
Crée ensuite une branche et capture l’état de base.
git status --short
git checkout -b refactor/safe-extract-order-total
npm test
npm run typecheck
npm run lint
Adapte les commandes à ton package.json. Si les tests échouent déjà avant le refactoring, note-le. Sinon, tu ne sauras plus si le problème vient du changement ou d’un état antérieur.
Use case 1: Renommer et extraire une petite fonction pure
Le premier exercice sûr consiste à améliorer les noms et extraire une fonction pure. Une fonction pure renvoie toujours le même résultat pour la même entrée et ne modifie ni base de données, ni email, ni API, ni état global. C’est un terrain idéal pour Claude Code, car le résultat est facile à tester.
// before: src/domain/order.ts
export function calc(o: { items: { p: number; q: number }[]; d?: number }) {
let t = 0;
for (const i of o.items) {
t += i.p * i.q;
}
if (o.d) {
t = t - o.d;
}
return Math.max(t, 0);
}
Le code est court, mais p, q et d cachent le sens métier. Demande à Claude Code d’écrire d’abord les tests, puis d’améliorer les noms.
Refactore prudemment la fonction calc dans src/domain/order.ts.
Exigences:
- Ajouter des tests unitaires qui fixent le comportement actuel avant modification
- Garder le nom exporté calc dans ce diff
- Améliorer les noms de variables et de types
- Conserver la règle: le total ne devient jamais négatif
- Exécuter npm test -- order après le changement
Un bon état après modification reste modeste.
// after: src/domain/order.ts
type OrderLine = {
price: number;
quantity: number;
};
type OrderInput = {
items: OrderLine[];
discount?: number;
};
export function calc(order: OrderInput): number {
const subtotal = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return Math.max(subtotal - (order.discount ?? 0), 0);
}
Tests copiables:
// src/domain/order.test.ts
import { describe, expect, it } from "vitest";
import { calc } from "./order";
describe("calc", () => {
it("multiplies price and quantity", () => {
expect(calc({ items: [{ price: 1200, quantity: 2 }] })).toBe(2400);
});
it("applies discount without returning a negative total", () => {
expect(calc({ items: [{ price: 500, quantity: 1 }], discount: 800 })).toBe(0);
});
});
Relis seulement les fichiers touchés.
git diff -- src/domain/order.ts src/domain/order.test.ts
npm test -- order
npm run typecheck
La bonne question n’est pas “le code est-il élégant ?”, mais “les mêmes entrées gardent-elles le même sens métier ?”. Vérifie le calcul, le nom exporté et la clarté des tests.
Use case 2: Supprimer any en typant d’abord les frontières
Réduire any est utile, mais le faire partout en une fois est une erreur classique. Commence par les frontières: réponses d’API, formulaires, configuration, webhooks ou fichiers importés. C’est là que les données inconnues entrent dans le système.
// before: src/lib/user-api.ts
export async function fetchUser(id: string): Promise<any> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
export function getDisplayName(user: any): string {
return user.profile.displayName || user.name;
}
Donne une cible étroite et décris le cas des données manquantes.
Réduis l'usage de any dans src/lib/user-api.ts.
Exigences:
- Ajouter un type pour la réponse API
- Garder l'URL fetch et la signification du retour
- Rendre getDisplayName sûr quand profile manque
- Ajouter des tests pour le comportement actuel du nom affiché
- Exécuter npm test -- user-api et npm run typecheck
Premier diff acceptable:
// after: src/lib/user-api.ts
export type UserResponse = {
id: string;
name: string;
profile?: {
displayName?: string;
};
};
export async function fetchUser(id: string): Promise<UserResponse> {
const response = await fetch(`/api/users/${id}`);
return response.json() as Promise<UserResponse>;
}
export function getDisplayName(user: UserResponse): string {
return user.profile?.displayName ?? user.name;
}
Ce cast ne valide pas les données au runtime. Si le projet a besoin d’une vraie validation d’entrée, fais un deuxième diff avec zod ou un parseur déjà présent. Ne mélange pas “retirer any” et “ajouter une bibliothèque de validation” dans le même premier changement.
// src/lib/user-api.test.ts
import { describe, expect, it } from "vitest";
import { getDisplayName, type UserResponse } from "./user-api";
describe("getDisplayName", () => {
it("uses profile displayName when present", () => {
const user: UserResponse = {
id: "u1",
name: "Masa",
profile: { displayName: "Masa I." },
};
expect(getDisplayName(user)).toBe("Masa I.");
});
it("falls back to name when profile is missing", () => {
expect(getDisplayName({ id: "u2", name: "Guest" })).toBe("Guest");
});
});
À la revue, cherche les raccourcis dangereux: as any, erreurs avalées, chaînes vides comme fallback, ou changements subtils sur les champs optionnels. Un diff plus typé peut quand même casser le comportement.
Use case 3: Diviser une grosse fonction seulement avec un harnais de tests
Les grosses fonctions sont attirantes, mais elles cachent souvent la logique métier. Commandes, facturation, permissions, notifications et imports mélangent validation, calcul, persistance et effets secondaires. Demande à Claude Code d’extraire une seule partie pure.
// before: src/services/order-service.ts
export async function createOrder(input: CreateOrderInput) {
if (input.items.length === 0) {
throw new Error("items required");
}
const subtotal = input.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const shippingFee = subtotal >= 10000 ? 0 : 800;
const total = subtotal + shippingFee;
const order = await db.order.create({
data: { userId: input.userId, subtotal, shippingFee, total },
});
await mailer.sendOrderCreated(order.id);
return order;
}
Le prompt doit aussi dire ce qui est hors périmètre.
Rends createOrder plus petite dans src/services/order-service.ts.
Dans ce diff:
- Extraire seulement le calcul des frais de livraison et du total dans une fonction pure
- La nommer calculateOrderTotals
- Ajouter des tests unitaires pour calculateOrderTotals
- Garder le même ordre d'écriture en base et d'envoi email
Ne pas faire:
- Changer le schéma DB
- Changer les messages d'erreur
- Changer la forme de réponse API
- Déplacer des fonctions non liées
- Reformater tout le fichier
État après:
// after: src/services/order-service.ts
export function calculateOrderTotals(items: OrderItem[]) {
const subtotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const shippingFee = subtotal >= 10000 ? 0 : 800;
return {
subtotal,
shippingFee,
total: subtotal + shippingFee,
};
}
Commandes de revue:
git diff --stat
git diff -- src/services/order-service.ts
git diff -- src/services/order-service.test.ts
npm test -- order-service
Si Claude Code reformate des zones sans lien:
Ce diff est trop grand.
Annule les changements de formatage et garde seulement l'extraction de calculateOrderTotals et ses tests.
Ne change pas le comportement externe, les textes d'erreur, les écritures DB ni l'ordre des emails.
Relire avec git diff, pas à l’intuition
L’explication de Claude Code aide, mais le diff est la source de vérité.
git diff --check
git diff --stat
git diff --name-only
git diff --word-diff -- src/domain/order.ts
| Zone | À vérifier |
|---|---|
| Comportement | Entrées, sorties, exceptions, HTTP status, ordre de persistance |
| Taille | Le changement tient dans une revue humaine |
| Tests | Le comportement existant est protégé |
| Types | Pas de nouveau as any, cast dangereux ou erreur ignorée |
| Effets | API, email, paiement, suppression et permissions gardent leur ordre |
| Résumé | Le résumé Claude Code correspond au diff réel |
Prompt de revue:
Relis ce git diff.
Vérifie:
- Si le changement dépasse le périmètre du refactoring
- Quel comportement n'est pas protégé par des tests
- S'il existe des casts dangereux ou erreurs avalées
- Quels fichiers un humain doit inspecter avec attention
Réponds avec:
- Sûr
- À confirmer par un humain
- À corriger
et donne les fichiers et raisons.
Pitfall: erreurs et risques à éviter
Premier failure: le prompt trop large.
Rends cette couche service plus propre.
Cela peut mélanger extraction, noms, erreurs, déplacement de fichiers et formatage. Préfère:
Extrais seulement le calcul de livraison de createOrder dans une fonction pure.
Ne change pas l'ordre de traitement, les messages d'erreur ni les valeurs de retour.
Deuxième risk: accepter un diff joli sans tests. Le code peut paraître plus lisible alors que les limites changent: remises, livraison gratuite, permissions, retry ou null. Troisième erreur: mélanger formatage et refactoring structurel. Si Prettier modifie des centaines de lignes, la vraie modification disparaît. Quatrième risque: ouvrir trop tôt les permissions de commandes. Commence par lecture, test, typecheck et lint; élargis ensuite.
Checklist réutilisable et CTA
## Refactoring checklist
- [ ] Le changement a un seul objectif
- [ ] Les tests de base ont été exécutés avant édition
- [ ] Le comportement before/after est équivalent
- [ ] Des tests existants ou nouveaux protègent le comportement
- [ ] git diff --stat est assez petit pour être relu
- [ ] git diff --check passe
- [ ] Aucun any, cast dangereux ou erreur avalée n'a été ajouté
- [ ] DB, email, paiement, suppression et permissions gardent leur ordre
Prompt final:
Exécute un seul diff de refactoring sûr.
Cible:
- src/services/order-service.ts
- src/services/order-service.test.ts
Critères de succès:
- Le comportement externe ne change pas
- calculateOrderTotals est extrait
- Les tests existants et ajoutés passent
- Rapporter git diff --stat et les commandes exécutées
Interdit:
- Changement de schéma DB
- Changement de réponse API
- Changement de message d'erreur
- Modification de fichiers non liés
Dans ma vérification, les deux habitudes les plus utiles ont été le plan sans édition et le résumé obligatoire du git diff. Combine ce workflow avec la checklist de revue Claude Code et les bonnes pratiques CLAUDE.md pour le rendre reproductible.
Pour une équipe qui veut industrialiser ce cadre, la formation Claude Code peut couvrir permissions, prompts, revue et workflow. Le refactoring automatisé devient rentable quand il réduit le risque de maintenance, pas quand il produit des diffs spectaculaires.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.