Rembourser une dette technique sans freeze: Branch by Abstraction sur un client HTTP legacy
Tutoriel actionnable pour migrer un client HTTP legacy (ex. axios) vers fetch en production sans freeze, via Branch by Abstraction, feature flags, codemods et garde-fous CI. Cible dev expérimenté, lisible en 8–12 min.
Sommaire
Rembourser une dette technique sans freeze: Branch by Abstraction sur un client HTTP legacy
Migrer un client HTTP legacy en production sans bloquer la roadmap, c’est possible. Dans cet article, on remplace axios (et un wrapper maison) par fetch, en s’appuyant sur Branch by Abstraction, des tests de caractérisation, un feature flag, des codemods et des garde-fous CI. Vous repartirez avec un plan actionnable, du code prêt à copier et un déroulé de déploiement progressif.
Objectif
Nous allons migrer un client HTTP legacy (par exemple axios) vers l’API fetch, côté front (Next.js) ou côté backend (Node.js/TypeScript), sans freeze produit. La stratégie repose sur:
- une façade stable (Branch by Abstraction) qui encapsule l’implémentation;
- des tests de caractérisation qui verrouillent le comportement;
- un feature flag pour activer fetch progressivement;
- un codemod pour automatiser une grande partie du refactoring;
- des garde-fous CI pour éviter tout recul.
Temps de lecture: 8–12 minutes. Le résultat: plus de maîtrise sur les dépendances, un bundle potentiellement plus léger côté front, et une base de code mieux testée et plus simple à maintenir.
Contexte et objectif mesurable
Cas d’étude: remplacer axios et un wrapper interne par une implémentation fetch munie d’une politique de retry maîtrisée. On s’assure de ne rien casser en amont (services), ni en SSR.
Definition of Done concrète:
- plus aucun import d’axios dans src/;
- réduction de bundle front de ≥ 15 kB gzip (valeur réaliste selon l’usage d’axios et utilitaires annexes);
- latence p95 non dégradée (écart ≤ 2% par service);
- même shape d’erreur qu’avant (pour ne pas casser la logique métier).
Périmètre: services HTTP au sein d’un monorepo TypeScript, consommés par:
- des pages/app routes Next.js (SSR/Edge/Client selon votre stack);
- des lambdas Node.js ou des services Node.
Risques principaux et comment les traiter:
- différences d’erreurs (axios structure ses erreurs différemment de fetch) → normalisation dans l’adapter + tests de caractérisation;
- timeouts (axios a un timeout natif; fetch nécessite AbortController) → implémentation explicite avec AbortController;
- intercepteurs (auth, headers) → reconstituer les hooks essentiels dans l’adapter;
- SSR (Node 18+ a fetch natif; en environnements plus anciens, utiliser undici) → verrouiller via tests SSR.
Plan de rollback: kill switch via feature flag. En cas de souci, désactiver immédiatement la voie fetch et revenir sur l’implémentation legacy. Pin de version sur axios dans package.json pour éviter une mise à jour involontaire pendant la migration:
{
"dependencies": {
"axios": "1.6.7"
}
}
Cartographier rapidement la dette
Commencez par mesurer l’empreinte actuelle:
rg "from 'axios'" -n src/
Vous obtenez la liste des fichiers et callsites. Exportez ces données pour quantifier les patterns utilisés (GET/POST, gestion d’intercepteurs, wrappers internes). Transformez en CSV pour un suivi hebdomadaire:
rg "from 'axios'" -n src/ | awk -F: '{print $1}' | sort | uniq | awk '{print $0",owner=?,complexity=?"}' > migration-http.csv
Ajoutez un compteur technique dans la CI pour empêcher la dette d’augmenter. Exemple de script Node qui échoue si le nombre d’imports axios augmente par rapport à un baseline:
// scripts/check-axios-count.mjs
import { execSync } from 'node:child_process';
import fs from 'node:fs';
const baselinePath = '.ci/baseline-axios-count.json';
const current = execSync(`rg "from 'axios'" -n src/ | wc -l`).toString().trim();
const currentCount = Number(current);
if (!fs.existsSync(baselinePath)) {
fs.mkdirSync('.ci', { recursive: true });
fs.writeFileSync(baselinePath, JSON.stringify({ count: currentCount }, null, 2));
console.log(`Baseline created with count=${currentCount}`);
process.exit(0);
}
const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8')).count;
if (currentCount > baseline) {
console.error(`Axios imports increased: ${currentCount} > ${baseline}`);
process.exit(1);
}
console.log(`Axios imports OK: ${currentCount} <= ${baseline}`);
Ce compteur, couplé à un tableau de chasse CSV (fichier, équipe, complexité estimée), vous permet de programmer des lots de migration réalistes et mesurables.
Branch by Abstraction: créer une façade stable
Créez une interface minimale et une façade @app/http qui encapsulent l’implémentation (axios d’abord, fetch ensuite) sans impacter la logique métier.
Interface TypeScript:
// src/app/http/types.ts
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
export interface RetryPolicy {
retries: number; // nombre de tentatives supplémentaires
backoffMs: number; // backoff initial
maxBackoffMs: number; // plafond de backoff
retryOn: number[]; // codes HTTP à retenter (ex: 429, 503)
}
export interface HttpRequest {
method: HttpMethod;
url: string;
headers?: Record<string, string>;
query?: Record<string, string | number | boolean>;
body?: unknown;
timeoutMs?: number;
retryPolicy?: RetryPolicy;
signal?: AbortSignal;
}
export interface HttpResponse<T = unknown> {
status: number;
headers: Record<string, string>;
data: T;
requestId?: string;
}
export interface HttpClient {
request<T = unknown>(req: HttpRequest): Promise<HttpResponse<T>>;
}
Adapter axios (implémentation legacy inchangée):
// src/app/http/axios-adapter.ts
import axios, { AxiosError } from 'axios';
import { HttpClient, HttpRequest, HttpResponse } from './types';
const baseURL = process.env.HTTP_BASE_URL ?? '';
const defaultTimeout = Number(process.env.HTTP_DEFAULT_TIMEOUT_MS ?? 10000);
const defaultHeaders: Record<string, string> = { 'content-type': 'application/json' };
function toQueryString(q?: HttpRequest['query']) {
if (!q) return '';
const params = new URLSearchParams();
for (const [k, v] of Object.entries(q)) params.set(k, String(v));
const s = params.toString();
return s.length ? `?${s}` : '';
}
function normalizeError(err: unknown, req: HttpRequest) {
const base = { method: req.method, url: req.url };
if (axios.isAxiosError(err)) {
const e = err as AxiosError;
const status = e.response?.status;
return Object.assign(new Error('HttpError'), {
name: 'HttpError',
status,
code: e.code ?? (status ? 'HTTP_ERROR' : 'NETWORK'),
details: {
...base,
status,
data: e.response?.data,
headers: e.response?.headers,
cause: e.message
}
});
}
return Object.assign(new Error('HttpError'), {
name: 'HttpError',
code: 'UNKNOWN',
details: { ...base, cause: String(err) }
});
}
export const axiosClient: HttpClient = {
async request<T>(req: HttpRequest): Promise<HttpResponse<T>> {
const url = `${baseURL}${req.url}${toQueryString(req.query)}`;
try {
const res = await axios.request<T>({
method: req.method,
url,
headers: { ...defaultHeaders, ...req.headers },
data: req.body,
timeout: req.timeoutMs ?? defaultTimeout,
validateStatus: () => true // parité: on laisse remonter les status non 2xx
});
const headers: Record<string, string> = {};
Object.entries(res.headers ?? {}).forEach(([k, v]) => (headers[k] = String(v)));
return {
status: res.status,
headers,
data: res.data as T,
requestId: headers['x-request-id']
};
} catch (e) {
throw normalizeError(e, req);
}
}
};
Façade et point d’entrée de l’adapter:
// src/app/http/index.ts
import { axiosClient } from './axios-adapter';
import { fetchClient } from './fetch-adapter'; // ajouté plus tard
import { HttpClient } from './types';
function isFetchEnabledFor(url: string): boolean {
// Bascule simple via env, extensible par fichier JSON ou provider (Unleash/LD)
const flag = process.env.USE_FETCH === 'true';
return flag && !/\/critical-endpoint/.test(url);
}
let currentImpl: HttpClient = axiosClient;
export const http: HttpClient = {
async request(req) {
const impl = isFetchEnabledFor(req.url) ? fetchClient : currentImpl;
return impl.request(req);
}
};
Ensuite, remplacez progressivement les imports d’axios par @app/http, sans toucher aux signatures métier:
Avant:
import axios from 'axios';
export async function getUser(id: string) {
const r = await axios.get(`/users/${id}`);
return r.data;
}
Après:
import { http } from '@app/http';
export async function getUser(id: string) {
const r = await http.request<{ id: string; name: string }>({ method: 'GET', url: `/users/${id}` });
return r.data;
}
La config (baseURL, auth, headers) est isolée dans l’adapter, réduisant le couplage et facilitant la migration.
Sécuriser avec des tests de caractérisation
Avant de changer l’implémentation, capturez le comportement actuel via des tests qui documentent les invariants: codes d’erreurs, retries, entêtes ajoutés.
Avec MSW (Node) pour simuler des réponses:
// test/http.adapter.spec.ts
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { http } from '../src/app/http';
import { HttpRequest } from '../src/app/http/types';
const server = setupServer(
rest.get('http://api.local/users/123', (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ id: '123', name: 'Ada' }), ctx.set('x-request-id', 'req-1'));
}),
rest.get('http://api.local/retry', (req, res, ctx) => {
// Simule 2 tentatives en 429 puis succès
const attempt = Number(req.headers.get('x-attempt') ?? '1');
if (attempt < 3) {
return res(ctx.status(429), ctx.json({ error: 'rate_limited' }));
}
return res(ctx.status(200), ctx.json({ ok: true }));
})
);
beforeAll(() => {
process.env.HTTP_BASE_URL = 'http://api.local';
server.listen({ onUnhandledRequest: 'error' });
});
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('retourne les données et entêtes (caractérisation)', async () => {
const res = await http.request<{ id: string; name: string }>({ method: 'GET', url: '/users/123' });
expect(res.status).toBe(200);
expect(res.data.name).toBe('Ada');
expect(res.requestId).toBe('req-1');
});
test('normalise les erreurs', async () => {
await expect(
http.request({ method: 'GET', url: '/unknown' as any })
).rejects.toMatchObject({ name: 'HttpError' });
});
Capturez aussi des snapshots d’erreurs normalisées pour détecter des régressions:
test('snapshot d’erreur 429', async () => {
await expect(
http.request({ method: 'GET', url: '/retry' })
).rejects.toMatchInlineSnapshot(`
Object {
"code": "HTTP_ERROR",
"details": Object {
"method": "GET",
"status": 429,
"url": "/retry",
},
"name": "HttpError",
"status": 429,
}
`);
});
Intégrez ces tests à la CI et bloquez toute PR qui modifie @app/http sans mettre à jour les tests correspondants.
Ajouter un feature flag dans l’adapter
Exposez une bascule USE_FETCH qui commande l’implémentation. Par défaut, restez en legacy (safe). Activez fetch service par service, ou par pourcentage de trafic.
Exemple de config JSON pour un contrôle fin:
{
"enabled": true,
"defaultPercentage": 0.1,
"overrides": [
{ "pattern": "^/users", "percentage": 0.5 },
{ "pattern": "^/billing", "percentage": 0.0 }
]
}
Usage dans l’adapter:
// src/app/http/flag.ts
import fs from 'node:fs';
type Rule = { pattern: string; percentage: number };
type Config = { enabled: boolean; defaultPercentage: number; overrides: Rule[] };
let cfg: Config = { enabled: false, defaultPercentage: 0, overrides: [] };
try {
const raw = fs.readFileSync(process.env.HTTP_FLAG_FILE ?? '', 'utf8');
cfg = JSON.parse(raw) as Config;
} catch {}
export function routeUsesFetch(url: string, seed: number = Math.random()): boolean {
if (!cfg.enabled) return false;
const match = cfg.overrides.find(r => new RegExp(r.pattern).test(url));
const p = (match?.percentage ?? cfg.defaultPercentage);
return seed < p;
}
Tracez le flag dans les logs/metrics (par exemple en ajoutant un tag fetch_enabled=true/false). Prévoyez un kill switch global (enabled=false) et la possibilité de cibler par route/service.
Implémenter l’alternative fetch avec parité fonctionnelle
Implémentez fetch avec timeout via AbortController, backoff exponentiel borné, normalisation d’erreurs, intercepteurs essentiels (auth, correlation-id) et métriques.
// src/app/http/fetch-adapter.ts
import { HttpClient, HttpRequest, HttpResponse } from './types';
const baseURL = process.env.HTTP_BASE_URL ?? '';
const defaultTimeout = Number(process.env.HTTP_DEFAULT_TIMEOUT_MS ?? 10000);
const defaultHeaders: Record<string, string> = { 'content-type': 'application/json' };
function toQueryString(q?: HttpRequest['query']) {
if (!q) return '';
const params = new URLSearchParams();
for (const [k, v] of Object.entries(q)) params.set(k, String(v));
const s = params.toString();
return s.length ? `?${s}` : '';
}
// Intercepteur d’auth simplifié
async function withAuth(headers: Record<string, string>) {
const token = process.env.HTTP_BEARER_TOKEN;
return token ? { ...headers, authorization: `Bearer ${token}` } : headers;
}
// Correlation-id
function withCorrelation(headers: Record<string, string>) {
const id = `corr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
return { ...headers, 'x-correlation-id': id };
}
function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}
function nextBackoff(prev: number, max: number) {
const n = Math.min(prev * 2, max);
return n;
}
function normalizeError(err: unknown, req: HttpRequest, status?: number) {
const base = { method: req.method, url: req.url, status };
const isAbort = (err as any)?.name === 'AbortError';
const code = isAbort ? 'TIMEOUT' : status ? 'HTTP_ERROR' : 'NETWORK';
return Object.assign(new Error('HttpError'), {
name: 'HttpError',
status,
code,
details: { ...base, cause: String((err as any)?.message ?? err) }
});
}
export const fetchClient: HttpClient = {
async request<T>(req: HttpRequest): Promise<HttpResponse<T>> {
const url = `${baseURL}${req.url}${toQueryString(req.query)}`;
const retry = req.retryPolicy ?? {
retries: 0,
backoffMs: 200,
maxBackoffMs: 2000,
retryOn: [429, 502, 503, 504]
};
let attempt = 0;
let backoff = retry.backoffMs;
while (true) {
const ctrl = new AbortController();
const timeout = setTimeout(() => ctrl.abort(), req.timeoutMs ?? defaultTimeout);
const headers0 = { ...defaultHeaders, ...req.headers };
const headers1 = await withAuth(headers0);
const headers = withCorrelation(headers1);
try {
const init: RequestInit = {
method: req.method,
headers,
body: req.body != null ? JSON.stringify(req.body) : undefined,
signal: req.signal ?? ctrl.signal
};
const startedAt = Date.now();
const res = await fetch(url, init);
clearTimeout(timeout);
const text = await res.text();
const contentType = res.headers.get('content-type') ?? '';
const data = contentType.includes('application/json') ? JSON.parse(text || '{}') : (text as unknown as T);
const headersObj: Record<string, string> = {};
res.headers.forEach((v, k) => (headersObj[k] = v));
const durationMs = Date.now() - startedAt;
// Exemple de log/metric
if (process.env.HTTP_LOG === 'true') {
console.log(JSON.stringify({
component: 'http',
impl: 'fetch',
method: req.method,
url: req.url,
status: res.status,
durationMs,
retries: attempt
}));
}
if (!res.ok) {
if (retry.retryOn.includes(res.status) && attempt < retry.retries) {
attempt += 1;
await sleep(backoff);
backoff = nextBackoff(backoff, retry.maxBackoffMs);
continue;
}
throw normalizeError(new Error(`HTTP ${res.status}`), req, res.status);
}
return {
status: res.status,
headers: headersObj,
data: data as T,
requestId: headersObj['x-request-id']
};
} catch (e) {
clearTimeout(timeout);
const isAbort = (e as any)?.name === 'AbortError';
if (isAbort) {
throw normalizeError(e, req);
}
// retry sur échecs réseau
if (attempt < retry.retries) {
attempt += 1;
await sleep(backoff);
backoff = nextBackoff(backoff, retry.maxBackoffMs);
continue;
}
throw normalizeError(e, req);
}
}
}
};
Ajoutez des métriques. Si vous utilisez OpenTelemetry:
// src/app/http/otel.ts
import { context, trace, SpanStatusCode } from '@opentelemetry/api';
export async function withSpan<T>(name: string, fn: () => Promise<T>): Promise<T> {
const tracer = trace.getTracer('http-adapter');
return await tracer.startActiveSpan(name, async (span) => {
try {
const result = await fn();
span.setStatus({ code: SpanStatusCode.OK });
span.end();
return result;
} catch (e: any) {
span.setStatus({ code: SpanStatusCode.ERROR, message: String(e?.message ?? e) });
span.recordException(e);
span.end();
throw e;
}
});
}
Enveloppez l’appel dans l’adapter si nécessaire:
// usage dans fetch-adapter.ts
// return await withSpan(`HTTP ${req.method} ${req.url}`, async () => { ... });
Déploiement progressif sans risque
Activez fetch sur un sous-ensemble de trafic:
- 1% sur des routes non critiques (par exemple /users, pas /payments);
- surveillez un dashboard: taux d’erreurs, latence p95/p99, timeouts, taille de bundle (côté front);
- élargissez 1% → 10% → 50% → 100% avec pauses d’observation (15–60 minutes selon trafic).
Exemple de bascule via fichier:
jq '.enabled=true | .defaultPercentage=0.01' http-flag.json > http-flag.tmp && mv http-flag.tmp http-flag.json
Plan de rollback:
- kill switch:
jq '.enabled=false' http-flag.json > http-flag.tmp && mv http-flag.tmp http-flag.json - si le problème semble logique/functional: revert Git de la PR qui a modifié l’adapter;
- figez à nouveau la version si vous aviez déverrouillé axios.
Sur Next.js, surveillez également la taille du bundle:
npx next build
npx next build --profile
npx source-map-explorer ".next/static/chunks/*.js" > bundles-report.txt
Automatiser la migration avec un codemod
Un codemod jscodeshift permet de remplacer les imports axios par @app/http et de mapper les usages simples. Exemple minimal traitant axios.get/post(url, data?, config?):
// codemods/axios-to-http.js
export default function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
let changed = false;
// Remplacer import axios
root.find(j.ImportDeclaration)
.filter(p => p.node.source.value === 'axios')
.forEach(p => {
changed = true;
j(p).replaceWith(
j.importDeclaration(
[j.importSpecifier(j.identifier('http'))],
j.literal('@app/http')
)
);
});
// axios.get(url, config) -> http.request({ method:'GET', url, ...config })
function replaceCall(method) {
root.find(j.CallExpression, {
callee: {
object: { name: 'axios' },
property: { name: method }
}
}).forEach(p => {
changed = true;
const args = p.node.arguments;
const urlArg = args[0];
const dataArg = ['post', 'put', 'patch'].includes(method) ? args[1] : null;
const cfgArg = ['post', 'put', 'patch'].includes(method) ? args[2] : args[1];
const props = [
j.objectProperty(j.identifier('method'), j.literal(method.toUpperCase()))
];
props.push(j.objectProperty(j.identifier('url'), urlArg));
if (dataArg) {
props.push(j.objectProperty(j.identifier('body'), dataArg));
}
if (cfgArg) {
props.push(j.spreadProperty(cfgArg));
}
j(p).replaceWith(
j.callExpression(
j.memberExpression(j.identifier('http'), j.identifier('request')),
[j.objectExpression(props)]
)
);
});
}
['get', 'delete', 'post', 'put', 'patch'].forEach(replaceCall);
return changed ? root.toSource() : null;
}
Exécution:
npx jscodeshift -t codemods/axios-to-http.js "src/**/*.ts?(x)" --dry
npx jscodeshift -t codemods/axios-to-http.js "src/**/*.ts?(x)"
Traitez les cas non triviaux (intercepteurs complexes, cancel tokens) manuellement. Validez chaque lot (ex: 30 fichiers/PR) en gardant le feature flag sur legacy (USE_FETCH=false): aucun changement de comportement n’est attendu.
Mettre des garde-fous dans la CI
Interdisez les imports axios hors adapter via ESLint:
{
"rules": {
"no-restricted-imports": ["error", {
"paths": [{
"name": "axios",
"message": "Utiliser @app/http"
}]
}]
},
"overrides": [{
"files": ["src/app/http/axios-adapter.ts"],
"rules": {
"no-restricted-imports": "off"
}
}]
}
Ajoutez un job CI qui échoue si des imports axios réapparaissent:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run lint
- run: npm test -- --ci
- name: Guard axios count
run: node scripts/check-axios-count.mjs
Avec Danger pour signaler un recul:
// dangerfile.js
import { danger, fail, warn } from 'danger';
const diff = danger.git.modified_files.concat(danger.git.created_files);
const touchedHttp = diff.filter(f => f.includes('src/app/http/'));
if (touchedHttp.length > 0) {
warn(`Fichiers @app/http modifiés: ${touchedHttp.join(', ')}`);
}
const axiosAdded = danger.git.diffForFile('src/**/*.ts');
if (axiosAdded && /from 'axios'/.test(axiosAdded.added)) {
fail('Import axios ajouté dans src/. Utiliser @app/http.');
}
Allouez explicitement un budget de temps hebdo (par exemple 10%) dans vos OKR pour solder la migration sans perturber la roadmap.
Nettoyage final et suppression des branches mortes
Quand 100% du trafic tourne sur fetch depuis au moins deux semaines sans incident:
- supprimez l’implémentation legacy dans l’adapter (axios-adapter) et la logique de bascule;
- retirez axios:
npm remove axios
npm i
- simplifiez l’API de l’adapter (chemin unique, suppression du flag);
- mettez à jour la documentation interne et les runbooks d’incident (nouvelle source de vérité: @app/http).
Pensez à régénérer le lockfile et à faire une passe de vérification des bundles:
rm -f package-lock.json pnpm-lock.yaml yarn.lock
npm i
npx next build
Mesurer le remboursement de la dette
Avant/après:
- taille du bundle front (gzip et brotli);
- latence p95/p99 des endpoints appelés;
- taux d’erreurs (4xx/5xx, timeouts);
- coût de maintenance (lignes d’utilitaires supprimées, intercepteurs simplifiés).
Tenez un registre de dette simple:
# debt-register.yaml
items:
- id: http-legacy-to-fetch
title: Migration axios -> fetch
owner: squad-frontend
status: done
startedAt: 2026-01-10
finishedAt: 2026-02-02
metrics:
bundleDeltaGzip: -18_000 # bytes
errorRateDeltaPct: -0.2
p95DeltaMs: -4
removedFiles: 6
notes:
- Feature flag progressif 1% -> 100%
- Tests de caractérisation MSW efficaces
- Monitoring OTel déployé sur les services critiques
nextTargets:
- Remplacer request-promise sur service batch
Faites une rétro: ce qui a marché (flags, codemods, tests), ce qui peut être amélioré (observabilité SSR, coverage des cas d’erreurs rares).
Checklist pratique et pièges à éviter
Avant d’activer fetch en production, vérifiez que vos tests de caractérisation couvrent les codes d’erreurs, les timeouts et les headers critiques (auth, x-request-id). Assurez-vous que le feature flag est déployé et pilotable rapidement (fichier JSON, Unleash, LaunchDarkly), et que les dashboards sont prêts (erreurs, latence p95/p99, retries, taille du bundle). Exécutez le codemod en dry-run puis par lots; complétez manuellement les cas d’intercepteurs avancés. Dans la CI, l’ESLint et la règle rg doivent empêcher la réintroduction d’axios. Documentez le rollback: kill switch, revert Git, version figée. Les pièges classiques: différences de semantics de timeout (AbortController obligatoire avec fetch), propagation correcte de l’AbortSignal à travers vos services, et compatibilité SSR (utilisez fetch natif de Node 18+ ou undici; évitez les polyfills non nécessaires). Ne migrez pas d’emblée les endpoints critiques; privilégiez un échantillon représentatif et réversible pour bâtir la confiance.
Checklist
- Relire
- Tester les commandes / snippets
- Publier
Ressources
- Branch by Abstraction, Martin Fowler: https://martinfowler.com/bliki/BranchByAbstraction.html
- MDN fetch: https://developer.mozilla.org/fr/docs/Web/API/Fetch_API
- AbortController: https://developer.mozilla.org/fr/docs/Web/API/AbortController
- MSW (Mock Service Worker): https://mswjs.io/
- OpenTelemetry JS: https://opentelemetry.io/docs/instrumentation/js/
- jscodeshift: https://github.com/facebook/jscodeshift
- undici (fetch pour Node): https://github.com/nodejs/undici
Conclusion
En appliquant Branch by Abstraction, des tests de caractérisation solides, un feature flag et des garde-fous CI, vous pouvez rembourser la dette d’un client HTTP legacy sans freeze produit. La migration axios → fetch devient une suite d’itérations sûres, mesurées et réversibles. Au-delà du gain de bundle et de la modernisation, vous obtenez un contrat d’API interne clair (@app/http) qui facilitera les évolutions futures et réduira le coût de maintenance.