Migración de una aplicación React de Axios a Fetch
Migra una app React de Axios a fetch con un wrapper seguro, manejo de errores, timeouts y un cliente listo para copiar sin regresiones.
Migrar una aplicación React de Axios a fetch es un cambio de bajo riesgo si se reemplaza la librería detrás de un wrapper en lugar de reescribir los puntos de llamada — y un campo minado de regresiones si no se hace así. Ambos clientes resuelven el mismo problema de manera diferente: fetch es el estándar nativo del navegador, sin dependencias, que te entrega los ingredientes en bruto, mientras que Axios agrupa funcionalidades ergonómicas — URL base, JSON automático, rechazo automático en respuestas de error, response.data, interceptores y timeouts — que deberás recrear tú mismo. Esta guía responde primero a la pregunta de si deberías migrar, y luego te ofrece una ruta de migración secuenciada y segura frente a regresiones para una aplicación React real, incluyendo un cliente completo listo para copiar y pegar.
Puntos clave
- La diferencia más peligrosa:
fetch()no rechaza en HTTP 4xx o 5xx — resuelve exitosamente y te entrega unaResponseconok: false, por lo que un error 500 atraviesa tu bloquetrycomo si fuera un 200, a menos que compruebesresponse.oktú mismo. - Migra el wrapper, no los puntos de llamada: si tu nuevo cliente
fetchexpone las mismas firmasget/post/patch/deletey devuelve las mismas promesas, React Query, tus mutaciones y tu manejo de errores nunca sabrán que Axios desapareció. - Usa
AbortSignal.timeout(8000)para darle afetchel timeout de solicitud que Axios tenía incorporado — está disponible como Baseline en todos los navegadores desde abril de 2024 y aborta con unTimeoutError. fetchya no es exclusivo del navegador: es un global estable en Node.js desde la versión 21, lo que elimina la última razón por la que el antiguo consejo de “mantén Axios para Node” tenía sentido.- El orden importa: primero cambia todas las importaciones al nuevo cliente, luego elimina
axiosy actualiza el lockfile — hazlo al revés y tu build se romperá a mitad de la migración.
Axios vs Fetch: el veredicto
Elige fetch cuando uses solo una pequeña parte de Axios y quieras eliminar una dependencia en tiempo de ejecución; mantén Axios cuando dependas de sus funcionalidades de mayor nivel. Axios te ofrece URL base, JSON automático, rechazo automático en respuestas fallidas, response.data e interceptores de forma gratuita; con fetch recrearás exactamente el subconjunto que tu aplicación realmente utiliza — y nada más. Para la mayoría de las SPAs React que ya centralizan las solicitudes en un único módulo de API, ese subconjunto es pequeño.
Las motivaciones para migrar son concretas:
- Tamaño del bundle. Axios es una dependencia adicional en tiempo de ejecución — aproximadamente 13–14 KB minificado y comprimido con gzip, dependiendo de la versión — mientras que un wrapper de
fetchson unas pocas centenas de líneas de código que tú controlas y puedes optimizar con tree-shaking. La cifra exacta varía con cada versión, así que mídela contra tu versión instalada con una herramienta como Bundlephobia en lugar de confiar en un número fijo. - Una dependencia menos / menor superficie de la cadena de suministro. Reducir dependencias también es una postura de seguridad. En marzo de 2026, atacantes publicaron dos versiones de Axios con backdoors en npm a través de una cuenta de mantenedor comprometida, y todos los proyectos con un rango de versión flotante las descargaron en la siguiente instalación. El post-mortem de los mantenedores (issue #10636 en GitHub) y el aviso de seguridad de Microsoft confirman que se trató de un compromiso de distribución/cuenta — dos versiones maliciosas publicadas, no un fallo en el código fuente de Axios — y las versiones maliciosas fueron retiradas en cuestión de horas; CISA también emitió una alerta. La lección no es “Axios es inseguro”; es que una dependencia menos es una cuenta menos que puede ser comprometida en tu nombre.
- Solo usas una fracción de sus funcionalidades. Si tu código es
axios.get(url).then(r => r.data), estás pagando el costo de una librería para hacer lo que ocho líneas defetchhacen.
Cuándo mantener Axios
Mantén Axios — la migración no vale la pena — si alguno de estos casos describe tu aplicación:
| Mantén Axios si dependes de… | Por qué fetch lo complica |
|---|---|
| Uso intensivo de interceptores de solicitud/respuesta | fetch no tiene sistema de interceptores; tendrías que reconstruirlo |
| UI de progreso de carga (upload progress) | Los cuerpos de solicitud de fetch no exponen eventos de progreso de carga; necesitarías XMLHttpRequest |
| Soporte para navegadores legacy (p. ej., IE11) | fetch nunca estuvo disponible en ellos |
Una amplia superficie de configuración específica de Axios (paramsSerializer, transformadores, adaptadores) | Recrearla cuesta más que lo que ahorra la dependencia |
Un motivo que ya no pertenece a esta lista es Node.js. fetch ya no es exclusivo del navegador: Node.js 18 (abril de 2022) incorporó un fetch global habilitado por defecto, y Node.js 21 (octubre de 2023) promovió fetch y Web Streams a estables. Para una comparación más detallada característica por característica, el artículo de LogRocket Axios vs Fetch breakdown cubre en detalle el progreso de descarga, streaming y cancelación.
Las diferencias de comportamiento que debes cubrir
Discover how at OpenReplay.com.
Axios realizaba seis cosas de forma implícita que fetch no hará — y cada una es un cambio de comportamiento silencioso: código que compila y se ejecuta, pero se comporta de manera diferente. Interioriza estas diferencias antes de escribir un wrapper.
fetchno rechaza en 4xx/5xx. Este es el punto más peligroso. Según la referencia de la Fetch API en MDN, la promesa se resuelve en cuanto el servidor responde con las cabeceras — incluso ante un estado HTTP de error. Un 500 se resuelve conresponse.ok === false. Axios rechaza automáticamente las respuestas no-2xx hacia.catch();fetchno lo hace. Debes comprobarresponse.oky lanzar el error tú mismo.- Sin JSON automático. Axios serializa los cuerpos de solicitud y parsea las respuestas por ti. Con
fetch, debes llamar aJSON.stringify()al enviar y aawait response.json()al recibir, además de establecerContent-Type: application/jsonmanualmente. - Sin
baseURL. Axios antepone una URL base configurada.fetchacepta una URL completa o una relativa al documento; debes concatenar las rutas tú mismo. - Sin
response.data. El cuerpo parseado se obtiene medianteawait response.json(), no a través de una propiedad.data. - Sin interceptores. No existe un hook incorporado para adjuntar tokens de autenticación o manejar errores 401 de forma global.
- Sin opción de timeout. Axios tiene una configuración de
timeout;fetchno la tiene. Debes proporcionar unAbortSignal.
Antes y después
La misma llamada, en Axios y en el cliente que construirás:
// Axios — rechazo automático en error, response.data, JSON automático
const { data } = await axios.get(`/users/${id}`);
return data;
// fetch (nativo) — debes comprobar ok, parsear manualmente, sin baseURL
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(res.statusText); // <-- sin esto, un 500 parece exitoso
return res.json();
La estrategia: migra el wrapper, no los puntos de llamada
No toques tus componentes, hooks ni llamadas de React Query. Reemplaza la librería detrás de tu superficie de API existente. Si tu aplicación ya enruta las solicitudes a través de un único módulo que expone get/post/patch/delete, solo necesitas hacer que la nueva implementación sea compatible en comportamiento — no compatible con Axios. Mantén firmas idénticas, devuelve las mismas promesas y lanza el mismo tipo de error, y todo lo que está aguas abajo permanecerá intacto. Esto es lo que hace que la migración sea aburrida en lugar de arriesgada.
La secuencia:
- Audita el uso. Confirma que las llamadas pasan por un único wrapper. Si no es así, tu primer PR es enrutarlas a través de uno — ese refactor es independiente de
fetchy seguro para desplegar por sí solo. - Establece el contrato de error. Si tu UI ramifica según
error.code === 'EXPIRED'o leeerror.status, tu nuevo cliente debe producir errores con esos mismos campos. Una discrepancia aquí es una rotura silenciosa. - Preserva la forma de la promesa. A React Query, SWR y tus mutaciones les importa que devuelvas una promesa que se resuelva con datos o se rechace con un error — no qué librería la produjo.
Un cliente fetch completo listo para copiar
Este api-client.ts completo cubre todas las diferencias de la comparación: concatenación de URL base, parámetros de consulta, fusión de cabeceras, cuerpo JSON, la comprobación de ok que rechaza respuestas de error, una clase de error personalizada, inyección de token de autenticación, manejo centralizado de errores 401 y un timeout mediante AbortSignal.timeout(). Incorpóralo a tu proyecto, apunta tus importaciones hacia él y tus puntos de llamada seguirán funcionando.
// 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 puede presentar un timeout de fetch como AbortError en lugar de 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");
}
// El punto peligroso: fetch resuelve en 4xx/5xx. Rechazar explícitamente.
if (!res.ok) {
let payload: any;
try { payload = await res.json(); } catch { payload = undefined; }
if (res.status === 401) { // equivalente al interceptor: manejo centralizado de 401
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" }),
};
Hay dos aspectos que vale la pena destacar. El timeout utiliza AbortSignal.timeout(), que devuelve un AbortSignal que se abortará automáticamente después de un tiempo especificado, y la señal aborta con una DOMException de tipo TimeoutError al agotarse el tiempo; desde abril de 2024, esta funcionalidad está disponible en todos los dispositivos y versiones de navegadores más recientes. Captura tanto TimeoutError como AbortError: en navegadores basados en Chromium, un timeout de fetch puede presentarse como AbortError en lugar de TimeoutError, así que maneja ambos nombres de forma defensiva. El cortocircuito para el código 204 evita llamar a .json() sobre un cuerpo vacío.
Recreando interceptores — y qué dejar en XHR
Tu wrapper es tu interceptor. Todo lo que hacían los interceptores de Axios — adjuntar un token Bearer, normalizar errores, redirigir en un 401 — vive en la función request() anterior, aplicada a cada llamada por construcción. No necesitas un sistema de plugins; necesitas un único punto de control, que ya tienes. Agregar reintentos ante un 429 o registro de solicitudes implica añadir unas pocas líneas a request(), no registrar un manejador.
Una cosa que deliberadamente no debes migrar: el progreso de carga (upload progress). Los cuerpos de solicitud de fetch en el navegador no exponen eventos de progreso de carga como lo hace XMLHttpRequest, así que mantén tu flujo de carga de archivos basado en XHR. No hay ningún problema en tener un enfoque híbrido: fetch para el 95% de las llamadas JSON, XHR para la barra de progreso de carga de archivos.
La secuencia de migración
Ejecuta estos pasos en orden. El orden es lo que la gente suele hacer mal.
- Cambia las importaciones primero. Apunta cada punto de llamada al nuevo cliente (
import { api } from "@/lib/api-client"). Como las firmas coinciden, esto es un buscar y reemplazar, no una reescritura. - Elimina Axios y luego actualiza el lockfile. Elimina
axiosdepackage.jsony ejecutanpm install(o el equivalente de tu gestor de paquetes) para actualizar el lockfile. Haz esto después del cambio de importaciones — si inviertes el orden, tu build se romperá a mitad de la migración con importaciones no resueltas. A partir de junio de 2026, el registro de npm lista Axios en la versión 1.18.1; si estás fijando una versión segura en otro lugar en lugar de eliminarlo, consulta el registro para conocer la versión actual. - Valida construyendo, no leyendo. Ejecuta tu suite de pruebas, una verificación de tipos TypeScript (
tsc --noEmit) y un build de producción. Una migración está terminada cuando la aplicación compila, pasa la verificación de tipos y se comporta correctamente — no cuando el diff tiene buen aspecto.
Ese último punto es donde se esconden las regresiones silenciosas. La regresión más peligrosa al migrar de Axios a fetch es la respuesta de error no gestionada: una solicitud que devolvió un 500 pero que, como fetch no lanza una excepción, fluyó como “éxito” y dejó al usuario mirando una interfaz vacía o rota sin que se mostrara ningún error. Las pruebas unitarias simulan el camino feliz y pasan; el log del servidor muestra el 500, por lo que el backend parece estar bien. La brecha entre “el servidor registró un error” y “el usuario no vio nada” es exactamente lo que la reproducción de sesión con captura de red pone de manifiesto — vincula la respuesta de error capturada con la interfaz rota que el usuario experimentó realmente, detectando la regresión que tus pruebas y los logs del servidor, por separado, no logran identificar. La comprobación !res.ok en el cliente anterior es lo que previene esta clase de error desde el principio; verificar que se activa en producción es lo que confirma que la migración está realmente terminada.
Un cliente basado en fetch te ofrece un árbol de dependencias más pequeño, control total sobre el comportamiento de errores y autenticación, y una cuenta de terceros menos en tu cadena de suministro — sin reescribir un solo componente, siempre que mantengas la superficie de API idéntica. El siguiente paso concreto: enruta las solicitudes de tu aplicación a través de un único módulo si aún no lo haces, luego incorpora el cliente anterior y deja que tu build, los tipos y las pruebas te indiquen cuándo has terminado.
Preguntas frecuentes
¿Lanza fetch un error ante una respuesta 404 o 500?
No. fetch solo rechaza su promesa ante un fallo de red o una solicitud que nunca se completa; un HTTP 404 o 500 se resuelve exitosamente con un objeto Response cuya propiedad ok es false. Esto significa que un error del servidor fluye a través de tu bloque try como si fuera un éxito, a menos que compruebes explícitamente response.ok y lances el error tú mismo. Axios se comporta de manera diferente y rechaza automáticamente cualquier estado no-2xx hacia el camino catch.
¿Sigo necesitando Axios para hacer solicitudes en Node.js?
No. fetch es ahora un global estable en Node.js desde la versión 21, lanzada en octubre de 2023, y estaba disponible como global experimental habilitado por defecto desde Node.js 18 en abril de 2022. El antiguo consejo de mantener Axios específicamente para solicitudes del lado del servidor ya no es válido. Puedes usar el mismo cliente basado en fetch tanto en el navegador como en código Node, aunque las versiones de Node anteriores a la 18 aún requieren un polyfill o una librería.
¿Cómo establezco un timeout de solicitud con fetch?
Pasa AbortSignal.timeout(milisegundos) como la opción signal, por ejemplo signal: AbortSignal.timeout(8000). Devuelve una señal que se aborta automáticamente después del tiempo especificado, rechazando el fetch con una DOMException de tipo TimeoutError. Está disponible como Baseline en todos los navegadores desde abril de 2024. En navegadores basados en Chromium, el rechazo puede presentarse como AbortError en lugar de TimeoutError, así que captura ambos nombres de forma defensiva al distinguir un timeout de una cancelación manual.
¿Romperá el cambio de Axios a fetch mi configuración de React Query o SWR?
No, siempre que tu nuevo cliente fetch devuelva las mismas promesas y exponga las mismas firmas de métodos. React Query, SWR y tus mutaciones solo se preocupan de que una llamada se resuelva con datos o se rechace con un error, no de qué librería lo produjo. Mantén tus firmas get, post, patch y delete idénticas y lanza el mismo tipo de error con los mismos campos en los que tu interfaz ramifica, y la capa de obtención de datos nunca sabrá que Axios desapareció.
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