12k
All articles

Migration d'une application React d'Axios vers Fetch

Migrez une app React dAxios vers fetch avec un wrapper sûr, la gestion des erreurs, des délais et un client prêt à copier pour éviter les régressions.

OpenReplay Team
OpenReplay Team
Migration d'une application React d'Axios vers Fetch

Migrer une application React d’Axios vers fetch est un changement à faible risque si vous remplacez la bibliothèque derrière une couche d’abstraction plutôt que de réécrire chaque point d’appel — et un véritable champ de mines en matière de régressions si vous ne le faites pas. Les deux clients résolvent le même problème différemment : fetch est le standard natif du navigateur, sans dépendance, qui vous fournit les ingrédients bruts, tandis qu’Axios regroupe des fonctionnalités ergonomiques — URL de base, JSON automatique, rejet automatique sur les réponses d’erreur, response.data, intercepteurs et délais d’expiration — que vous devez recréer vous-même. Ce guide répond d’abord à la question « dois-je migrer ? », puis vous propose une séquence de migration sécurisée et sans régression pour une vraie application React, avec un client complet prêt à copier-coller.

Points clés à retenir

  • La différence la plus dangereuse : fetch() ne rejette pas sur les erreurs HTTP 4xx ou 5xx — il se résout avec succès et vous retourne une Response avec ok: false, de sorte qu’une erreur 500 traverse votre bloc try comme si c’était un 200, à moins que vous ne vérifiez response.ok vous-même.
  • Migrez la couche d’abstraction, pas les points d’appel : si votre nouveau client fetch expose les mêmes signatures get/post/patch/delete et retourne les mêmes promesses, React Query, vos mutations et votre gestion des erreurs ne sauront jamais qu’Axios a disparu.
  • Utilisez AbortSignal.timeout(8000) pour donner à fetch le délai d’expiration qu’Axios avait intégré — cette fonctionnalité est disponible sur l’ensemble des navigateurs depuis avril 2024 et interrompt la requête avec une TimeoutError.
  • fetch n’est plus réservé aux navigateurs : c’est un global stable dans Node.js depuis la version 21, ce qui supprime la dernière raison pour laquelle l’ancien conseil « gardez Axios pour Node » était pertinent.
  • L’ordre est important : remplacez d’abord chaque import par le nouveau client, puis supprimez axios et actualisez le lockfile — faites-le dans l’ordre inverse et votre build se cassera en pleine migration.

Axios vs Fetch : le verdict

Choisissez fetch si vous n’utilisez qu’une infime partie d’Axios et souhaitez supprimer une dépendance à l’exécution ; conservez Axios si vous vous appuyez sur ses fonctionnalités de plus haut niveau. Axios vous offre gratuitement une URL de base, le JSON automatique, le rejet automatique sur les réponses en échec, response.data et des intercepteurs ; avec fetch, vous recréez exactement le sous-ensemble dont votre application a réellement besoin — et rien de plus. Pour la plupart des SPA React qui centralisent déjà les requêtes dans un seul module API, ce sous-ensemble est réduit.

Les motivations pour migrer sont concrètes :

  • Taille du bundle. Axios est une dépendance à l’exécution supplémentaire — environ 13 à 14 Ko minifié et compressé en gzip, selon la version — tandis qu’une couche d’abstraction fetch représente quelques centaines de lignes de code que vous possédez et pouvez tree-shaker. Le chiffre exact évolue à chaque version, mesurez-le par rapport à votre version installée avec un outil comme Bundlephobia plutôt que de vous fier à un chiffre figé.
  • Une dépendance en moins / surface de chaîne d’approvisionnement réduite. Réduire les dépendances est aussi une posture de sécurité. En mars 2026, des attaquants ont publié deux versions d’Axios contenant des backdoors sur npm via un compte de mainteneur compromis, et chaque projet utilisant une plage de version flottante les a téléchargées lors de la prochaine installation. Le post-mortem des mainteneurs (issue GitHub #10636) et l’avis de sécurité de Microsoft confirment tous deux qu’il s’agissait d’une compromission de distribution et de compte — deux versions malveillantes publiées, et non d’une faille dans le code source d’Axios — et les mauvaises versions ont été retirées en quelques heures ; la CISA a également émis une alerte. La leçon n’est pas « Axios est dangereux » ; c’est qu’une dépendance en moins représente un compte de moins susceptible d’être compromis en votre nom.
  • Vous n’utilisez qu’une infime partie. Si votre code se résume à axios.get(url).then(r => r.data), vous payez pour une bibliothèque qui fait ce que huit lignes de fetch accomplissent.

Quand conserver Axios

Conservez Axios — la migration n’en vaut pas la peine — si l’une de ces situations décrit votre application :

Conservez Axios si vous vous appuyez sur…Pourquoi fetch rend cela difficile
Une utilisation intensive des intercepteurs de requêtes/réponsesfetch ne dispose pas de système d’intercepteurs ; vous devriez le reconstruire
Une interface de progression des uploadsLes corps de requêtes fetch n’exposent pas d’événements de progression d’upload ; vous avez besoin de XMLHttpRequest
La prise en charge de navigateurs anciens (ex. IE11)fetch n’y a jamais été disponible
Une large surface de configuration spécifique à Axios (paramsSerializer, transformateurs, adaptateurs)La recréer coûte plus que ce que la suppression de la dépendance rapporte

Une raison qui n’a plus sa place dans cette liste est Node.js. fetch n’est plus réservé aux navigateurs : Node.js 18 (avril 2022) a intégré un global fetch activé par défaut, et Node.js 21 (octobre 2023) a promu fetch et les Web Streams au statut stable. Pour une comparaison fonctionnalité par fonctionnalité plus approfondie, l’article de LogRocket Axios vs Fetch couvre en détail la progression des téléchargements, le streaming et l’annulation.

Les écarts de comportement à combler

Axios effectuait implicitement six choses que fetch ne fait pas — et chacune représente un changement de comportement silencieux : du code qui compile et s’exécute, mais se comporte différemment. Assimilez ces écarts avant d’écrire une couche d’abstraction.

  1. fetch ne rejette pas sur les erreurs 4xx/5xx. C’est le piège classique. Selon la référence MDN de l’API Fetch, la promesse se résout dès que le serveur répond avec des en-têtes — même en cas de statut d’erreur HTTP. Une erreur 500 se résout avec response.ok === false. Axios rejette automatiquement les statuts non-2xx dans .catch() ; fetch ne le fait pas. Vous devez vérifier response.ok et lever l’exception vous-même.
  2. Pas de JSON automatique. Axios sérialise les corps de requêtes et analyse les réponses pour vous. Avec fetch, vous appelez JSON.stringify() à l’envoi et await response.json() à la réception, et vous définissez Content-Type: application/json manuellement.
  3. Pas de baseURL. Axios préfixe une URL de base configurée. fetch prend une URL complète ou relative au document ; vous assemblez les chemins vous-même.
  4. Pas de response.data. Le corps analysé se trouve derrière await response.json(), et non sur une propriété .data.
  5. Pas d’intercepteurs. Il n’existe pas de hook intégré pour attacher des tokens d’authentification ou gérer les erreurs 401 globalement.
  6. Pas d’option de délai d’expiration. Axios dispose d’une configuration timeout ; fetch n’en a pas. Vous fournissez un AbortSignal.

Avant et après

Le même appel, en Axios et dans le client que vous allez construire :

// Axios — rejet automatique sur erreur, response.data, JSON automatique
const { data } = await axios.get(`/users/${id}`);
return data;

// fetch (brut) — doit vérifier ok, doit analyser, pas de baseURL
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(res.statusText); // <-- sans ceci, une erreur 500 ressemble à un succès
return res.json();

La stratégie : migrez la couche d’abstraction, pas les points d’appel

Ne touchez pas à vos composants, hooks ou appels React Query. Remplacez la bibliothèque derrière votre surface API existante. Si votre application route déjà les requêtes via un seul module exposant get/post/patch/delete, vous devez uniquement rendre la nouvelle implémentation compatible en termes de comportement — pas compatible avec Axios. Conservez des signatures identiques, retournez les mêmes promesses, et levez le même type d’erreur, et tout ce qui est en aval reste intact. C’est ce qui rend la migration ennuyeuse plutôt que risquée.

La séquence :

  1. Auditez l’utilisation. Confirmez que les appels passent par une seule couche d’abstraction. Si ce n’est pas le cas, votre première PR consiste à les y faire transiter — ce refactoring est indépendant de fetch et peut être livré séparément en toute sécurité.
  2. Faites correspondre le contrat d’erreur. Si votre interface branche sur error.code === 'EXPIRED' ou lit error.status, votre nouveau client doit produire des erreurs avec ces mêmes champs. Une incompatibilité ici est une rupture silencieuse.
  3. Préservez la forme des promesses. React Query, SWR et vos mutations s’attendent à ce que vous retourniez une promesse qui se résout en données ou rejette avec une erreur — peu importe quelle bibliothèque l’a produite.

Un client fetch complet prêt à copier

Ce api-client.ts complet comble chaque écart identifié dans la comparaison : assemblage de l’URL de base, paramètres de requête, fusion des en-têtes, corps JSON, la vérification ok qui rejette les réponses en erreur, une classe d’erreur personnalisée, injection du token d’authentification, gestion centralisée des erreurs 401 et un délai d’expiration via AbortSignal.timeout(). Intégrez-le, pointez vos imports vers lui, et vos points d’appel continuent de fonctionner.

// lib/api-client.ts
const BASE_URL = import.meta.env.VITE_API_URL ?? "/api";
const DEFAULT_TIMEOUT = 8000;

export class ApiClientError extends Error {
  status: number;
  code?: string;
  data?: unknown;
  constructor(message: string, status: number, code?: string, data?: unknown) {
    super(message);
    this.name = "ApiClientError";
    this.status = status;
    this.code = code;
    this.data = data;
  }
}

type Query = Record<string, string | number | boolean | undefined>;

interface RequestOptions extends Omit<RequestInit, "body"> {
  body?: unknown;
  params?: Query;
  timeout?: number;
}

function buildUrl(path: string, params?: Query): string {
  const base = BASE_URL.endsWith("/") ? BASE_URL : `${BASE_URL}/`;
  const url = new URL(path.replace(/^\//, ""), new URL(base, window.location.origin));
  if (params) {
    for (const [key, value] of Object.entries(params)) {
      if (value !== undefined) url.searchParams.set(key, String(value));
    }
  }
  return url.toString();
}

async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
  const { body, params, timeout = DEFAULT_TIMEOUT, headers, ...rest } = options;
  const token = localStorage.getItem("token");

  let res: Response;
  try {
    res = await fetch(buildUrl(path, params), {
      ...rest,
      headers: {
        "Content-Type": "application/json",
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
        ...headers,
      },
      body: body !== undefined ? JSON.stringify(body) : undefined,
      signal: AbortSignal.timeout(timeout),
    });
  } catch (err) {
    // Chromium may surface a fetch timeout as AbortError rather than TimeoutError.
    if (err instanceof DOMException && (err.name === "TimeoutError" || err.name === "AbortError")) {
      throw new ApiClientError("Request timed out", 0, "TIMEOUT");
    }
    throw new ApiClientError("Network error", 0, "NETWORK");
  }

  // The footgun: fetch resolves on 4xx/5xx. Reject explicitly.
  if (!res.ok) {
    let payload: any;
    try { payload = await res.json(); } catch { payload = undefined; }

    if (res.status === 401) {              // interceptor-equivalent: central 401 handling
      localStorage.removeItem("token");
      window.location.assign("/login");
    }
    throw new ApiClientError(
      payload?.message ?? res.statusText,
      res.status,
      payload?.code,
      payload,
    );
  }

  if (res.status === 204) return undefined as T;
  return (await res.json()) as T;
}

export const api = {
  get:    <T>(path: string, options?: RequestOptions) => request<T>(path, { ...options, method: "GET" }),
  post:   <T>(path: string, body?: unknown, options?: RequestOptions) => request<T>(path, { ...options, method: "POST", body }),
  patch:  <T>(path: string, body?: unknown, options?: RequestOptions) => request<T>(path, { ...options, method: "PATCH", body }),
  put:    <T>(path: string, body?: unknown, options?: RequestOptions) => request<T>(path, { ...options, method: "PUT", body }),
  delete: <T>(path: string, options?: RequestOptions) => request<T>(path, { ...options, method: "DELETE" }),
};

Deux points méritent d’être soulignés. Le délai d’expiration utilise AbortSignal.timeout(), qui retourne un AbortSignal qui s’annulera automatiquement après un délai spécifié, et le signal s’annule avec une TimeoutError DOMException à l’expiration ; depuis avril 2024, cette fonctionnalité fonctionne sur l’ensemble des appareils et versions de navigateurs récents. Interceptez à la fois TimeoutError et AbortError : dans les navigateurs basés sur Chromium, un délai d’expiration de fetch peut se manifester comme une AbortError plutôt qu’une TimeoutError, gérez donc les deux noms de manière défensive. Le court-circuit sur 204 évite d’appeler .json() sur un corps vide.

Recréer les intercepteurs — et ce qu’il faut laisser à XHR

Votre couche d’abstraction est votre intercepteur. Tout ce que les intercepteurs Axios faisaient — attacher un token Bearer, normaliser les erreurs, rediriger sur une erreur 401 — se trouve dans la fonction request() ci-dessus, appliqué à chaque appel par construction. Vous n’avez pas besoin d’un système de plugins ; vous avez besoin d’un seul point de passage, que vous possédez déjà. Ajouter une logique de nouvelle tentative sur une erreur 429 ou la journalisation des requêtes signifie ajouter quelques lignes à request(), et non enregistrer un gestionnaire.

Une chose à délibérément ne pas migrer : la progression des uploads. Les corps de requêtes fetch dans les navigateurs n’exposent pas d’événements de progression d’upload comme le fait XMLHttpRequest, donc conservez votre flux d’upload existant basé sur XHR. Il n’y a aucune honte à adopter une approche hybride : fetch pour les 95% d’appels JSON, XHR pour la barre de progression des uploads de fichiers.

La séquence de migration

Exécutez ces étapes dans l’ordre. L’ordre est précisément ce que les gens font mal.

  1. Remplacez d’abord les imports. Faites pointer chaque point d’appel vers le nouveau client (import { api } from "@/lib/api-client"). Comme les signatures correspondent, il s’agit d’un remplacement global, pas d’une réécriture.
  2. Supprimez Axios, puis actualisez le lockfile. Supprimez axios de package.json et exécutez npm install (ou l’équivalent de votre gestionnaire de paquets) pour mettre à jour le lockfile. Faites cela après le remplacement des imports — inversez l’ordre et votre build se cassera en pleine migration avec des imports non résolus. En juin 2026, le registre npm liste Axios à la version 1.18.1 ; si vous épinglez une version sûre ailleurs plutôt que de la supprimer, vérifiez le registre pour la version actuelle.
  3. Validez en buildant, pas en lisant. Exécutez votre suite de tests, une vérification de types TypeScript (tsc --noEmit), et un build de production. Une migration est terminée quand l’application se build, passe la vérification de types et se comporte correctement — pas quand le diff semble propre.

C’est dans ce dernier point que se cachent les régressions silencieuses. La régression Axios→fetch la plus dangereuse est la réponse d’erreur non gérée : une requête qui a retourné une erreur 500 mais qui, parce que fetch ne lève pas d’exception, a été traitée comme un « succès » et a laissé l’utilisateur face à une interface vide ou cassée sans qu’aucune erreur ne soit affichée. Les tests unitaires simulent le chemin nominal et passent ; le journal du serveur montre l’erreur 500, donc le backend semble correct. L’écart entre « le serveur a journalisé une erreur » et « l’utilisateur n’a rien vu » est exactement ce que la relecture de session avec capture réseau met en évidence — elle relie la réponse d’erreur capturée à l’interface réellement cassée que l’utilisateur a vécue, détectant la régression que vos tests et vos journaux serveur manquent chacun individuellement. La vérification !res.ok dans le client ci-dessus est ce qui prévient cette catégorie de bugs en premier lieu ; vérifier qu’elle se déclenche en production confirme que la migration est réellement terminée.

Un client basé sur fetch vous offre un arbre de dépendances plus léger, un contrôle total sur le comportement des erreurs et de l’authentification, et un compte tiers de moins dans votre chaîne d’approvisionnement — sans réécrire un seul composant, à condition de conserver une surface API identique. La prochaine étape concrète : faites transiter les requêtes de votre application par un seul module si ce n’est pas déjà le cas, puis intégrez le client ci-dessus et laissez votre build, vos types et vos tests vous indiquer quand vous avez terminé.

FAQ

Est-ce que fetch lève une erreur sur une réponse 404 ou 500 ?

Non. fetch ne rejette sa promesse qu'en cas de défaillance réseau ou de requête qui ne se termine jamais ; un HTTP 404 ou 500 se résout avec succès avec un objet Response dont la propriété ok est false. Cela signifie qu'une erreur serveur traverse votre bloc try comme si c'était un succès, à moins que vous ne vérifiez explicitement response.ok et ne leviez l'exception vous-même. Axios se comporte différemment et rejette automatiquement tout statut non-2xx dans le chemin catch.

Ai-je encore besoin d'Axios pour effectuer des requêtes dans Node.js ?

Non. fetch est désormais un global stable dans Node.js depuis la version 21, publiée en octobre 2023, et était disponible comme global expérimental activé par défaut depuis Node.js 18 en avril 2022. L'ancien conseil de conserver Axios spécifiquement pour les requêtes côté serveur n'est plus valable. Vous pouvez utiliser le même client basé sur fetch dans le code navigateur et Node, bien que les versions de Node antérieures à la version 18 nécessitent encore un polyfill ou une bibliothèque.

Comment définir un délai d'expiration de requête avec fetch ?

Passez AbortSignal.timeout(millisecondes) comme option signal, par exemple signal: AbortSignal.timeout(8000). Cela retourne un signal qui s'annule automatiquement après le délai spécifié, rejetant le fetch avec une TimeoutError DOMException. Cette fonctionnalité est disponible sur l'ensemble des navigateurs depuis avril 2024. Dans les navigateurs basés sur Chromium, le rejet peut se manifester comme une AbortError plutôt qu'une TimeoutError, gérez donc les deux noms de manière défensive pour distinguer un délai d'expiration d'une annulation manuelle.

Le passage d'Axios à fetch va-t-il casser mon configuration React Query ou SWR ?

Non, tant que votre nouveau client fetch retourne les mêmes promesses et expose les mêmes signatures de méthodes. React Query, SWR et vos mutations s'intéressent uniquement au fait qu'un appel se résout en données ou rejette avec une erreur, peu importe quelle bibliothèque l'a produit. Conservez vos signatures get, post, patch et delete identiques et levez le même type d'erreur avec les mêmes champs sur lesquels votre interface branche, et la couche de récupération de données ne saura jamais qu'Axios a disparu.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.