12k
All articles

Migration einer React-App von Axios zu Fetch

Migrieren Sie eine React-App von Axios zu fetch mit sicherem Wrapper, Fehlerbehandlung, Timeouts und einem Copy-Paste-Client ohne Regressionen.

OpenReplay Team
OpenReplay Team
Migration einer React-App von Axios zu Fetch

Die Migration einer React-App von Axios zu fetch ist eine risikoarme Änderung, sofern die Bibliothek hinter einem Wrapper ausgetauscht wird, anstatt alle Aufrufstellen neu zu schreiben — andernfalls wird sie zum Minenfeld für Regressionen. Beide Clients lösen dasselbe Problem auf unterschiedliche Weise: fetch ist der native, abhängigkeitsfreie Browser-Standard, der rohe Bausteine liefert, während Axios Komfortfunktionen bündelt — Base-URL, automatisches JSON, automatische Ablehnung bei Fehlerantworten, response.data, Interceptors und Timeouts — die manuell nachgebaut werden müssen. Dieser Leitfaden beantwortet zunächst die Frage, ob ein Wechsel sinnvoll ist, und zeigt anschließend einen sequenziellen, regressionssicheren Migrationspfad für eine echte React-App — einschließlich eines vollständig kopierbaren Clients.

Wichtigste Erkenntnisse

  • Der gefährlichste Unterschied: fetch() lehnt HTTP-4xx- oder 5xx-Antworten nicht ab — die Promise wird erfolgreich aufgelöst und liefert eine Response mit ok: false. Ein 500er läuft daher durch den try-Block, als wäre er ein 200er, sofern response.ok nicht explizit geprüft wird.
  • Den Wrapper migrieren, nicht die Aufrufstellen: Wenn der neue fetch-Client dieselben get/post/patch/delete-Signaturen bereitstellt und dieselben Promises zurückgibt, merken React Query, Mutations und die Fehlerbehandlung nichts davon, dass Axios entfernt wurde.
  • AbortSignal.timeout(8000) verwenden, um fetch den in Axios eingebauten Request-Timeout zu geben — seit April 2024 browserübergreifend als Baseline verfügbar und bricht mit einem TimeoutError ab.
  • fetch ist nicht mehr ausschließlich für Browser: Seit Node.js v21 ist es ein stabiles Global, womit der letzte Grund entfällt, warum der alte Rat „Axios für Node behalten” je galt.
  • Die Reihenfolge ist entscheidend: Zuerst alle Imports auf den neuen Client umstellen, dann axios entfernen und die Lockfile aktualisieren — in umgekehrter Reihenfolge bricht der Build mitten in der Migration.

Axios vs. Fetch: Das Urteil

fetch wählen, wenn nur ein kleiner Teil von Axios genutzt wird und eine Laufzeitabhängigkeit entfallen soll; Axios behalten, wenn dessen höherwertige Features intensiv genutzt werden. Axios bietet Base-URL, automatisches JSON, automatische Ablehnung bei fehlgeschlagenen Antworten, response.data und Interceptors ohne zusätzlichen Aufwand; mit fetch wird genau die Teilmenge nachgebaut, die die App tatsächlich benötigt — und nichts darüber hinaus. Für die meisten React-SPAs, die Requests bereits in einem zentralen API-Modul bündeln, ist diese Teilmenge überschaubar.

Die Gründe für eine Migration sind konkret:

  • Bundle-Größe. Axios ist eine zusätzliche Laufzeitabhängigkeit — je nach Version etwa 13–14 KB minifiziert und gzip-komprimiert — während ein fetch-Wrapper aus wenigen hundert Zeilen eigenem, tree-shakefähigem Code besteht. Der genaue Wert ändert sich mit jedem Release; daher empfiehlt sich eine Messung gegen die installierte Version mit einem Tool wie Bundlephobia statt der Verwendung einer fest kodierten Zahl.
  • Eine Abhängigkeit weniger / kleinere Supply-Chain-Angriffsfläche. Weniger Abhängigkeiten bedeutet auch eine bessere Sicherheitslage. Im März 2026 veröffentlichten Angreifer über ein kompromittiertes Maintainer-Konto zwei manipulierte Axios-Versionen auf npm, die bei der nächsten Installation von jedem Projekt mit einem offenen Versionsbereich heruntergeladen wurden. Sowohl der Post-mortem-Bericht der Maintainer (GitHub Issue #10636) als auch Microsofts Security Advisory bestätigen, dass es sich um einen Distributions- bzw. Account-Kompromiss handelte — zwei bösartige veröffentlichte Versionen, kein Fehler im Axios-Quellcode — und die schadhaften Versionen wurden innerhalb weniger Stunden entfernt; CISA gab ebenfalls eine Warnung heraus. Die Lehre daraus lautet nicht „Axios ist unsicher”, sondern: Eine Abhängigkeit weniger bedeutet ein Konto weniger, das im eigenen Namen kompromittiert werden kann.
  • Nur ein Bruchteil wird genutzt. Wenn der Code auf axios.get(url).then(r => r.data) reduziert ist, wird für eine Bibliothek bezahlt, die das erledigt, was acht Zeilen fetch leisten.

Wann Axios behalten

Axios sollte beibehalten werden — eine Migration lohnt sich nicht —, wenn einer der folgenden Punkte zutrifft:

Axios behalten, wenn folgendes genutzt wird…Warum fetch es erschwert
Intensiver Einsatz von Request-/Response-Interceptorsfetch hat kein Interceptor-System; es müsste neu implementiert werden
Upload-Fortschritt-UIfetch-Request-Bodies stellen keine Upload-Progress-Events bereit; XMLHttpRequest wird benötigt
Legacy-Browser-Unterstützung (z. B. IE11)fetch war dort nie verfügbar
Umfangreiche Axios-spezifische Konfiguration (paramsSerializer, Transformer, Adapter)Die Nachimplementierung kostet mehr als die Abhängigkeit einspart

Ein Grund, der nicht mehr auf diese Liste gehört, ist Node.js. fetch ist keine reine Browser-Angelegenheit mehr: Node.js 18 (April 2022) lieferte ein standardmäßig aktiviertes globales fetch, und Node.js 21 (Oktober 2023) stufte fetch und Web Streams auf stabil hoch. Für einen detaillierten Feature-Vergleich behandelt LogRockets Axios vs. Fetch-Gegenüberstellung Download-Fortschritt, Streaming und Abbruch im Detail.

Die Verhaltensunterschiede, die geschlossen werden müssen

Axios erledigte sechs Dinge implizit, die fetch nicht tut — und jedes davon ist eine stille Verhaltensänderung: Code, der kompiliert und ausgeführt wird, sich aber anders verhält. Diese Unterschiede sollten verinnerlicht werden, bevor ein Wrapper geschrieben wird.

  1. fetch lehnt 4xx/5xx nicht ab. Dies ist die klassische Fehlerquelle. Laut MDNs Fetch-API-Referenz wird die Promise aufgelöst, sobald der Server mit Headern antwortet — auch bei einem HTTP-Fehlerstatus. Ein 500er löst sich mit response.ok === false auf. Axios lehnt Nicht-2xx-Antworten automatisch in .catch() ab; fetch nicht. response.ok muss manuell geprüft und ein Fehler explizit geworfen werden.
  2. Kein automatisches JSON. Axios serialisiert Request-Bodies und parst Antworten automatisch. Mit fetch wird beim Senden JSON.stringify() aufgerufen, beim Empfangen await response.json(), und Content-Type: application/json muss manuell gesetzt werden.
  3. Keine baseURL. Axios stellt eine konfigurierte Base-URL voran. fetch nimmt eine vollständige URL oder eine relative zur aktuellen Seite; Pfade müssen manuell zusammengesetzt werden.
  4. Kein response.data. Der geparste Body liegt hinter await response.json(), nicht in einer .data-Eigenschaft.
  5. Keine Interceptors. Es gibt keinen eingebauten Hook zum globalen Anhängen von Auth-Tokens oder zur Behandlung von 401-Antworten.
  6. Keine Timeout-Option. Axios hat eine timeout-Konfiguration; fetch nicht. Ein AbortSignal muss bereitgestellt werden.

Vorher und nachher

Derselbe Aufruf, in Axios und im zu bauenden Client:

// Axios — auto-reject on error, response.data, auto-JSON
const { data } = await axios.get(`/users/${id}`);
return data;

// fetch (raw) — must check ok, must parse, no baseURL
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(res.statusText); // <-- ohne diese Prüfung sieht ein 500er wie Erfolg aus
return res.json();

Die Strategie: Den Wrapper migrieren, nicht die Aufrufstellen

Komponenten, Hooks oder React-Query-Aufrufe werden nicht angefasst. Die Bibliothek wird hinter der bestehenden API-Oberfläche ausgetauscht. Wenn die App Requests bereits über ein einziges Modul mit get/post/patch/delete bündelt, muss nur die neue Implementierung verhaltenskompatibel sein — nicht Axios-kompatibel. Identische Signaturen beibehalten, dieselben Promises zurückgeben, denselben Fehlertyp werfen — und alles Nachgelagerte bleibt unberührt. Das macht die Migration planbar statt riskant.

Die Schrittfolge:

  1. Nutzung prüfen. Sicherstellen, dass Aufrufe über einen einzigen Wrapper laufen. Falls nicht, ist der erste PR, sie dorthin zu leiten — dieses Refactoring ist unabhängig von fetch und kann eigenständig ausgeliefert werden.
  2. Den Fehlervertrag abgleichen. Wenn die UI auf error.code === 'EXPIRED' verzweigt oder error.status liest, muss der neue Client Fehler mit denselben Feldern erzeugen. Eine Abweichung hier ist ein stiller Bruch.
  3. Promise-Form beibehalten. React Query, SWR und Mutations erwarten eine Promise, die zu Daten aufgelöst oder mit einem Fehler abgelehnt wird — nicht, welche Bibliothek sie erzeugt hat.

Ein vollständiger Fetch-Client zum Kopieren

Dieses vollständige api-client.ts schließt jeden Unterschied aus dem Vergleich: Base-URL-Zusammensetzung, Query-Parameter, Header-Zusammenführung, JSON-Body, die ok-Prüfung, die Fehlerantworten ablehnt, eine benutzerdefinierte Fehlerklasse, Auth-Token-Injektion, zentrale 401-Behandlung und ein Timeout via AbortSignal.timeout(). Einfach einfügen, Imports darauf ausrichten, und die Aufrufstellen funktionieren weiterhin.

// 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" }),
};

Zwei Details verdienen besondere Erwähnung. Der Timeout verwendet AbortSignal.timeout(), das ein AbortSignal zurückgibt, das nach einer festgelegten Zeit automatisch abbricht; das Signal bricht beim Timeout mit einer TimeoutError-DOMException ab. Seit April 2024 ist dieses Feature browserübergreifend verfügbar. Sowohl TimeoutError als auch AbortError sollten abgefangen werden: In Chromium-basierten Browsern kann ein fetch-Timeout als AbortError statt als TimeoutError auftreten, daher defensiv beide Namen behandeln. Die 204-Kurzschlusslogik verhindert den Aufruf von .json() auf einem leeren Body.

Interceptors nachbauen — und was bei XHR bleiben sollte

Der Wrapper ist der Interceptor. Alles, was Axios-Interceptors erledigten — ein Bearer-Token anhängen, Fehler normalisieren, bei 401 weiterleiten — befindet sich in der request()-Funktion oben und wird konstruktionsbedingt auf jeden Aufruf angewendet. Kein Plugin-System wird benötigt; ein einziger Engpass genügt, der bereits vorhanden ist. Retry-on-429 oder Request-Logging hinzuzufügen bedeutet, wenige Zeilen zu request() hinzuzufügen, nicht einen Handler zu registrieren.

Etwas, das bewusst nicht migriert werden sollte: Upload-Fortschritt. Browser-fetch-Request-Bodies stellen keine Upload-Progress-Events bereit wie XMLHttpRequest, daher sollte der bestehende XHR-basierte Upload-Flow beibehalten werden. Ein hybrider Ansatz ist völlig legitim: fetch für die 95 % der JSON-Aufrufe, XHR für den Datei-Upload-Fortschrittsbalken.

Die Migrationsschrittfolge

Diese Schritte in der angegebenen Reihenfolge ausführen. Die Reihenfolge ist der Teil, bei dem Fehler am häufigsten passieren.

  1. Zuerst Imports umstellen. Alle Aufrufstellen auf den neuen Client zeigen lassen (import { api } from "@/lib/api-client"). Da die Signaturen übereinstimmen, ist dies ein Suchen-und-Ersetzen, kein Neuschreiben.
  2. Axios entfernen, dann die Lockfile aktualisieren. axios aus package.json löschen und npm install (oder das Äquivalent des verwendeten Paketmanagers) ausführen, um die Lockfile zu aktualisieren. Dies nach dem Import-Wechsel tun — in umgekehrter Reihenfolge bricht der Build mitten in der Migration mit unaufgelösten Imports. Stand Juni 2026 listet die npm-Registry Axios in Version 1.18.1; wer andernorts eine sichere Version pinnt statt sie zu entfernen, sollte die Registry auf das aktuelle Release prüfen.
  3. Durch Bauen validieren, nicht durch Lesen. Die Testsuite ausführen, einen TypeScript-Typecheck (tsc --noEmit) durchführen und einen Produktions-Build erstellen. Eine Migration ist abgeschlossen, wenn die App baut, den Typecheck besteht und sich korrekt verhält — nicht wenn das Diff sauber aussieht.

Genau hier verbergen sich stille Regressionen. Die gefährlichste Axios→fetch-Regression ist die unbehandelte Fehlerantwort: Ein Request, der einen 500er zurückgab, aber weil fetch keinen Fehler wirft, als „Erfolg” durchlief und den Nutzer mit einer leeren oder defekten UI zurückließ — ohne Fehlermeldung. Unit-Tests mocken den Happy Path und bestehen; das Server-Log zeigt den 500er, also sieht das Backend in Ordnung aus. Die Lücke zwischen „Server hat einen Fehler protokolliert” und „Nutzer hat nichts gesehen” ist genau das, was Session-Replay mit Netzwerkaufzeichnung aufdeckt — es verknüpft die aufgezeichnete Fehlerantwort mit der tatsächlich defekten UI, die der Nutzer erlebt hat, und erfasst so die Regression, die Tests und Server-Logs jeweils einzeln übersehen. Die !res.ok-Prüfung im obigen Client verhindert diese Fehlerklasse von vornherein; die Verifizierung, dass sie in der Produktion ausgelöst wird, bestätigt, dass die Migration tatsächlich abgeschlossen ist.

Ein fetch-basierter Client liefert einen schlankeren Abhängigkeitsbaum, vollständige Kontrolle über Fehler- und Auth-Verhalten und ein Drittanbieter-Konto weniger in der Supply Chain — ohne eine einzige Komponente neu zu schreiben, sofern die API-Oberfläche identisch bleibt. Der nächste konkrete Schritt: App-Requests über ein einziges Modul bündeln, falls noch nicht geschehen, dann den obigen Client einfügen und Build, Types und Tests zeigen lassen, wann die Migration abgeschlossen ist.

Häufig gestellte Fragen

Wirft fetch bei einer 404- oder 500-Antwort einen Fehler?

Nein. fetch lehnt seine Promise nur bei einem Netzwerkfehler oder einem Request ab, der nie abgeschlossen wird; ein HTTP-404 oder -500 wird erfolgreich mit einem Response-Objekt aufgelöst, dessen ok-Eigenschaft false ist. Das bedeutet, ein Serverfehler läuft durch den try-Block, als wäre er ein Erfolg, sofern nicht explizit response.ok geprüft und selbst ein Fehler geworfen wird. Axios verhält sich anders und lehnt jeden Nicht-2xx-Status automatisch in den catch-Pfad ab.

Wird Axios noch für Requests in Node.js benötigt?

Nein. fetch ist seit Version 21, veröffentlicht im Oktober 2023, ein stabiles Global in Node.js und war seit Node.js 18 im April 2022 als experimentelles, standardmäßig aktiviertes Global verfügbar. Der ältere Rat, Axios speziell für serverseitige Requests beizubehalten, gilt nicht mehr. Derselbe fetch-basierte Client kann in Browser- und Node-Code verwendet werden, obwohl ältere Node-Versionen vor 18 noch ein Polyfill oder eine Bibliothek benötigen.

Wie wird ein Request-Timeout mit fetch gesetzt?

AbortSignal.timeout(Millisekunden) als signal-Option übergeben, zum Beispiel signal: AbortSignal.timeout(8000). Es wird ein Signal zurückgegeben, das nach der angegebenen Zeit automatisch abbricht und den fetch mit einer TimeoutError-DOMException ablehnt. Seit April 2024 ist es browserübergreifend als Baseline verfügbar. In Chromium-basierten Browsern kann die Ablehnung als AbortError statt als TimeoutError auftreten; daher defensiv beide Namen abfangen, wenn ein Timeout von einem manuellen Abbruch unterschieden werden soll.

Bricht der Wechsel von Axios zu fetch mein React-Query- oder SWR-Setup?

Nein, sofern der neue fetch-Client dieselben Promises zurückgibt und dieselben Methodensignaturen bereitstellt. React Query, SWR und Mutations erwarten nur, dass ein Aufruf zu Daten aufgelöst oder mit einem Fehler abgelehnt wird — nicht, welche Bibliothek ihn erzeugt hat. get-, post-, patch- und delete-Signaturen identisch halten und denselben Fehlertyp mit denselben Feldern werfen, auf die die UI verzweigt, und die Datenabrufschicht merkt nichts davon, dass Axios entfernt wurde.

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.