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.
Receta
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.
Ejemplo Funcional
Paso 1: Crear la Instancia de Axios
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.createproduce una instancia aislada — no interferirá con otras librerías que usan el default global deaxios.- El interceptor de solicitud añade auth. El interceptor de respuesta maneja 401 globalmente.
- Las guardias de
typeof windowlo mantienen seguro si el módulo se importa en el servidor.
Paso 2: Funciones API Tipadas
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.
Paso 3: Usar en Componentes Client
// 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>
);
}Paso 4: Usar con SWR (Axios como el fetcher)
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");Paso 5: Mutaciones (POST, PUT, DELETE)
// 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>
);
}Análisis Profundo
Orden de Ejecución de Interceptores
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.
Cancelación de Solicitud
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();
}, []);Progreso de Carga
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;
}Reintento con Interceptores
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);
});Uso en el Lado del Servidor (Route Handlers / Server Actions)
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;
});Trampas
-
Axios añade ~13KB comprimido. Si tu única necesidad es solicitudes GET simples con
useEffect, plainfetcho 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 usaaxios.create()para la instancia de tu proyecto. Las librerías o código de terceros que también importanaxiosheredarán tus interceptores si modificas el default. -
Los interceptores que acceden a
windowse 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.responsepuede serundefined. Los errores de red y timeouts no tienen respuesta. Siempre compruebaerror.response?.statusantes de acceder a datos de respuesta, o usaaxios.isAxiosError(err)para type narrowing. -
Axios serializa objetos como JSON automáticamente. A diferencia de
fetch, no necesitasJSON.stringify(body). Pero para cargas deFormData, debes reemplazar el encabezadoContent-Typeo Axios enviará JSON. -
axios.isCancel()devuelvetruesolo paraCancelTokenheredado. Si usasAbortController(recomendado), comprueba el nombre del error conerror.name === "CanceledError"o usaaxios.isCancel(error)que maneja ambos. -
Los interceptores de respuesta ven la
AxiosResponsecompleta. Si transformas respuestas en un interceptor (p. ej.,return response.data), los tipos de TypeScript deapi.get<T>()serán incorrectos porque esperan unaAxiosResponse<T>, noT. 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.
Alternativas
| 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 |
Relacionado
- Utilidad Centralizada de Fetch con SWR — enfoque SWR-first para fetching centralizado
- Data Fetching en Server Components — patrones de fetch del lado del servidor
- Almacenamiento en caché — capas de almacenamiento en caché de Next.js
- Streaming — carga progresiva de datos con Suspense