Utilidad Centralizada de Fetch con Axios
Construye un cliente API centralizado con Axios - interceptores para autenticación, manejo de errores, reintentos y transformaciones de solicitud/respuesta en una instancia configurada.
Busca en todas las páginas de la documentación
Construye un cliente API centralizado con Axios - interceptores para autenticación, manejo de errores, reintentos y transformaciones de solicitud/respuesta en una instancia configurada.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de receta de referencia rápida -- lista para copiar y pegar.
// lib/axios.ts
import axios from "axios";
export const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL ?? "https://api.example.com",
timeout: 10_000,
headers: { "Content-Type": "application/json" },
});
// Adjunta el token a cada solicitud
api.interceptors.request.use((config) => {
const token = typeof window !== "undefined"
? localStorage.getItem("token")
: null;
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Normaliza errores
api.interceptors.response.use(
(res) => res,
(error) => {
if (error.response?.status === 401) {
window.location.href = "/login";
}
return Promise.reject(error);
}
);// Cualquier componente o Server Action
import { api } from "@/lib/axios";
const { data } = await api.get<Post[]>("/posts");Cuándo usarlo: Necesitas interceptores (inyección de auth, manejo de errores global, logging de solicitudes), reintentos automáticos, cancelación de solicitudes o seguimiento de progreso de carga - cosas que plain fetch requiere plomería manual.
Una instancia configurada reemplaza todas las llamadas fetch brutas. Cada solicitud pasa por los mismos interceptores.
// lib/axios.ts
import axios, {
type AxiosError,
type AxiosRequestConfig,
type InternalAxiosRequestConfig,
} from "axios";
// ---------- Instancia ----------
export const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL ?? "",
timeout: 10_000,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
// ---------- Interceptor de Solicitud ----------
api.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// Adjunta token de auth
if (typeof window !== "undefined") {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
return config;
},
(error) => Promise.reject(error)
);
// ---------- Interceptor de Respuesta ----------
api.interceptors.response.use(
(response) => response,
(error: AxiosError<{ message?: string }>) => {
const status = error.response?.status;
// Redirección global 401
if (status === 401 && typeof window !== "undefined") {
localStorage.removeItem("token");
window.location.href = "/login";
}
// Enriquece el mensaje de error desde el cuerpo de la respuesta de la API
const serverMessage = error.response?.data?.message;
if (serverMessage) {
error.message = serverMessage;
}
return Promise.reject(error);
}
);Decisiones clave:
axios.create produce una instancia aislada - no interferirá con otras librerías que usan el default global de axios.typeof window lo mantienen seguro si el módulo se importa en el servidor.Envuelve la instancia en funciones tipadas para que los consumidores no traten directamente con AxiosResponse.
// lib/api.ts
import { api } from "@/lib/axios";
// ---------- Tipos ----------
export type Post = { id: number; title: string; body: string };
export type User = { id: number; name: string; email: string };
export type Comment = { id: number; postId: number; body: string };
// ---------- Posts ----------
export const postsApi = {
getAll: () =>
api.get<Post[]>("/posts").then((r) => r.data),
getById: (id: number) =>
api.get<Post>(`/posts/${id}`).then((r) => r.data),
create: (data: Omit<Post, "id">) =>
api.post<Post>("/posts", data).then((r) => r.data),
update: (id: number, data: Partial<Post>) =>
api.put<Post>(`/posts/${id}`, data).then((r) => r.data),
delete: (id: number) =>
api.delete(`/posts/${id}`),
};
// ---------- Usuarios ----------
export const usersApi = {
getMe: () =>
api.get<User>("/me").then((r) => r.data),
getById: (id: number) =>
api.get<User>(`/users/${id}`).then((r) => r.data),
};¿Por qué .then(r => r.data)? Axios envuelve respuestas en { data, status, headers, ... }. Desenvuelve en la capa de API significa que cada consumidor obtiene datos limpios tipados sin acceder a .data.
// components/posts-list.tsx
"use client";
import { useEffect, useState } from "react";
import { postsApi, type Post } from "@/lib/api";
export function PostsList() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
postsApi
.getAll()
.then(setPosts)
.catch((err) => {
if (!controller.signal.aborted) {
setError(err.message);
}
})
.finally(() => setLoading(false));
return () => controller.abort();
}, []);
if (loading) return <p>Cargando...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}Axios y SWR funcionan bien juntos. Usa Axios como el fetcher de SWR para lo mejor de ambos - interceptores de Axios, almacenamiento en caché/revalidación de SWR.
// hooks/use-api.ts
"use client";
import useSWR, { type SWRConfiguration } from "swr";
import { api } from "@/lib/axios";
import type { AxiosError } from "axios";
const axiosFetcher = <T,>(url: string): Promise<T> =>
api.get<T>(url).then((r) => r.data);
export function useApi<T>(
path: string | null,
options?: SWRConfiguration<T, AxiosError>
) {
return useSWR<T, AxiosError>(path, axiosFetcher, options);
}// Ahora cada componente obtiene interceptores + almacenamiento en caché de SWR
const { data: posts } = useApi<Post[]>("/posts");
const { data: user } = useApi<User>("/me");// components/create-post-form.tsx
"use client";
import { useState } from "react";
import { postsApi } from "@/lib/api";
export function CreatePostForm({ onCreated }: { onCreated: () => void }) {
const [title, setTitle] = useState("");
const [submitting, setSubmitting] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setSubmitting(true);
try {
await postsApi.create({ title, body: "" });
setTitle("");
onCreated(); // dispara re-fetch del padre
} catch (err) {
alert(err instanceof Error ? err.message : "Falló crear publicación");
} finally {
setSubmitting(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button disabled={submitting}>
{submitting ? "Creando..." : "Crear"}
</button>
</form>
);
}Los interceptores se ejecutan en un pipeline. Múltiples interceptores de solicitud se ejecutan en orden de registro inverso. Múltiples interceptores de respuesta se ejecutan en orden de registro.
Solicitud: [interceptor 2] → [interceptor 1] → red
Respuesta: [interceptor 1] → [interceptor 2] → tu código
Esto importa cuando tienes tanto un interceptor de logging como un interceptor de auth - el orden de registro determina cuál ve la solicitud primero.
Axios soporta AbortController (misma API que fetch):
useEffect(() => {
const controller = new AbortController();
api.get<Post[]>("/posts", { signal: controller.signal })
.then((r) => setPosts(r.data))
.catch((err) => {
if (!axios.isCancel(err)) setError(err.message);
});
return () => controller.abort();
}, []);Una de las ventajas más fuertes de Axios sobre fetch:
async function uploadFile(file: File) {
const formData = new FormData();
formData.append("file", file);
const { data } = await api.post("/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress(event) {
const percent = Math.round((event.loaded * 100) / (event.total ?? 1));
console.log(`Carga: ${percent}%`);
},
});
return data;
}Implementa reintento automático para fallos transitorios:
api.interceptors.response.use(undefined, async (error: AxiosError) => {
const config = error.config as InternalAxiosRequestConfig & { _retryCount?: number };
if (!config) return Promise.reject(error);
config._retryCount = config._retryCount ?? 0;
const status = error.response?.status ?? 0;
// Reintenta en 5xx o errores de red, hasta 3 veces
if (config._retryCount < 3 && (status >= 500 || status === 0)) {
config._retryCount += 1;
const delay = 2 ** config._retryCount * 500; // backoff exponencial
await new Promise((r) => setTimeout(r, delay));
return api.request(config);
}
return Promise.reject(error);
});Axios funciona en Route Handlers y Server Actions, pero sin interceptores que accedan a window. Crea una instancia separada del servidor:
// lib/axios-server.ts
import axios from "axios";
export const serverApi = axios.create({
baseURL: process.env.API_URL, // sin prefijo NEXT_PUBLIC_ -- solo servidor
timeout: 10_000,
headers: { "Content-Type": "application/json" },
});
// La auth del lado del servidor usa tokens de servicio, no localStorage
serverApi.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${process.env.API_SERVICE_TOKEN}`;
return config;
});Axios añade ~13KB comprimido. Si tu única necesidad es solicitudes GET simples con useEffect, plain fetch o el fetcher incorporado de SWR es más ligero. Axios se rentabiliza cuando necesitas interceptores, progreso de carga o reintentos automáticos.
No mutees el default global de axios. Siempre usa axios.create() para la instancia de tu proyecto. Las librerías o código de terceros que también importan axios heredarán tus interceptores si modificas el default.
Los interceptores que acceden a window se rompen en el servidor. La misma instancia no puede ejecutarse seguramente en ambos entornos. Crea instancias separadas del cliente (lib/axios.ts) y del servidor (lib/axios-server.ts).
error.response puede ser undefined. Los errores de red y timeouts no tienen respuesta. Siempre comprueba error.response?.status antes de acceder a datos de respuesta, o usa axios.isAxiosError(err) para type narrowing.
Axios serializa objetos como JSON automáticamente. A diferencia de fetch, no necesitas JSON.stringify(body). Pero para cargas de FormData, debes reemplazar el encabezado Content-Type o Axios enviará JSON.
axios.isCancel() devuelve true solo para CancelToken heredado. Si usas AbortController (recomendado), comprueba el nombre del error con error.name === "CanceledError" o usa axios.isCancel(error) que maneja ambos.
Los interceptores de respuesta ven la AxiosResponse completa. Si transformas respuestas en un interceptor (p. ej., return response.data), los tipos de TypeScript de api.get<T>() serán incorrectos porque esperan una AxiosResponse<T>, no T. Mantén el desenvolvimiento en las funciones de API, no en interceptores.
Los interceptores de reintento pueden causar bucles infinitos. Siempre registra el conteo de reintentos en el objeto de configuración y establece un máximo. Sin un tope, un error 500 persistente reintentos indefinidamente.
| Enfoque | Cuándo usarlo |
|---|---|
SWR con fetch | Almacenamiento en caché y revalidación del lado del cliente sin necesidades de interceptor |
| TanStack Query + Axios | Ciclo de vida completo de query/mutación con Axios como el transporte |
ky (fetch wrapper) | Quieres hooks tipo-interceptor pero prefieres la superficie API nativa de fetch |
Plain fetch + función wrapper | Proyectos mínimos - sin dependencias, solo una wrapper tipada |
ofetch (unjs) | Fetch universal que funciona en Node, browser y workers con reintento automático |
Revisado por Chris St. John·Última actualización: 9 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥