5 F5. Hablar con una API desde el frontend
fetch, axios, React Query y HttpClient — el cliente del contrato
Todas las formas de llamar una API desde el cliente: fetch nativo, axios, React Query/SWR y el HttpClient de Angular — y por qué en una app real no basta un fetch suelto. Este capítulo es el puente: el cliente que consume el contrato que tú construirás en la Sección II.
5.1 El problema
Tu componente necesita las tareas del servidor. La primera aproximación:
const res = await fetch("/api/tasks");
const tasks = await res.json();Funciona — hasta que necesitas loading mientras carga, error si falla, reintento si la red se cae, caché para no pedir lo mismo, y mutación para el POST de crear. Ahí es donde un fetch suelto se queda corto y nace el ecosistema de clientes.
5.2 Conceptos nuevos
- U fetch nativo: la Promise del navegador para HTTP — la base de todo.
- U Estados de la llamada: loading / error / data — el ciclo completo de una petición real.
- U Cliente HTTP con extras: axios (interceptors, JSON auto), React Query (caché + estados).
El estado es todo lo que el programa recuerda: las variables, la sesión, lo que muestra la UI. Stateless (sin estado) = el servidor no recuerda nada entre requests — cada request trae todo lo necesario.
Un caché es una copia rápida de algo costoso de obtener: el resultado de una query pesada, la sesión. Redis es el caché estándar. La regla de oro: cachear es fácil, invalidar (saber cuándo el caché ya no vale) es lo difícil.
- U Observable (Angular): stream reactivo en vez de Promise — el modelo de RxJS.
Asíncrono = empezar algo sin esperar sentado a que termine: pides la pizza (async) y sigues trabajando; cuando llega, te avisan. Lo opuesto a síncrono (esperar parado). Vital cuando la espera es larga: red, disco, bases de datos.
5.3 Implementación: de menos a más
async function getTasks() {
const res = await fetch("/api/tasks");
if (!res.ok) throw new Error(`HTTP ${res.status}`); // 404 no lanza solo
return res.json();
}
// POST con body JSON:
await fetch("/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "ship v1" }),
});Tres detalles: fetch no rechaza en 4xx/5xx (revisa res.ok), el body se parsea con .json(), y credentials: "include" envía cookies.
import axios from "axios";
const { data } = await axios.get("/api/tasks"); // JSON automático
await axios.post("/api/tasks", { title: "ship v1" });
// interceptor: agrega auth a TODOS los requests
axios.interceptors.request.use((cfg) => {
cfg.headers.Authorization = `Bearer ${token}`;
return cfg;
});Axios lanza error en 4xx/5xx (mejor que fetch) y los interceptors son middleware del lado cliente.
import { useQuery, useMutation } from "@tanstack/react-query";
function Tasks() {
const { data, isLoading, error } = useQuery({
queryKey: ["tasks"],
queryFn: () => fetch("/api/tasks").then((r) => r.json()),
});
const createTask = useMutation({
mutationFn: (t) =>
fetch("/api/tasks", { method: "POST", body: JSON.stringify(t) }),
});
if (isLoading) return <p>Cargando…</p>;
if (error) return <p>Error: {error.message}</p>;
return <ul>{data.map((t) => <li key={t.id}>{t.title}</li>)}</ul>;
}
useQuery resuelve el ciclo completo: loading/error/data + caché + revalidación + reintentos — lo que a mano son 40 líneas de useState/ useEffect.
// Angular devuelve un Observable, no una Promise
this.http.get<Task[]>("/api/tasks").subscribe({
next: (tasks) => (this.tasks = tasks),
error: (err) => console.error(err.status),
});Observable vs Promise: el Observable es frío — no hay request hasta .subscribe() — puede emitir varios valores y se cancela con unsubscribe(). Angular trae interceptors nativos para auth.
5.4 ¿Por qué tantas opciones?
Lo único que necesitas llevarte: todas hablan HTTP igual — la diferencia es cuánto te ayudan con el ciclo completo (loading, error, caché). fetch es la base; axios/React Query agregan la gestión que una app real necesita.
Promise (fetch, axios): un valor futuro, se dispara al crearla, no se cancela nativamente (AbortController aparte). Observable (HttpClient): un stream de valores posibles, no hace nada hasta suscribirse, se cancela con unsubscribe(). Angular eligió Observables porque las UIs son flujos de eventos continuos — modelo más potente, curva más empinada.
Quiero llamar una API REST desde React. Muéstrame la evolución completa:
(1) fetch suelto con useState+useEffect (loading, error, data), (2) axios
con un interceptor para Authorization, (3) TanStack Query con useQuery y
useMutation. Para cada versión explica qué problema de la anterior
resuelve. El endpoint es GET/POST /api/tasks que devuelve {id,title,done}.
Esta decisión es material: llamar la API debe pasar; con qué depende de cuánto ciclo de vida necesitas.
| Alternativa | Qué te cuesta | Cuándo elegirla |
|---|---|---|
fetch + useState a mano |
Tú gestionas loading/error/caché | Una llamada puntual, demo, aprender |
| axios | Una dependencia, su API | Auth con interceptors, apps medianas |
| React Query / SWR | Conceptos de caché/invalidación | React serio — casi siempre la respuesta |
| HttpClient (Angular) | RxJS — la curva más alta | Angular: obligado, y es bueno |
| GraphQL client | Cambia el contrato a GraphQL | Si el backend ya es GraphQL |
5.5 Errores comunes
fetchsin revisarres.ok: un 404 llega como “éxito” y rompes al hacer.json()o al pintar.- useEffect con fetch sin cleanup: doble llamada en StrictMode, race conditions si el componente se desmonta.
- Guardar el token en el interceptor… leído una vez: el token cambia al hacer login; léelo dentro del interceptor cada request.
La autenticación responde “¿quién eres?” — login, contraseña, token. Se confunde con autorización (“¿qué puedes hacer?”), que es la pregunta siguiente. Primero te identificas, luego te dejan o no pasar.
- Manejar loading/error a mano cuando ya usas React Query: reinventar la rueda dentro de la librería que la resuelve.
5.6 Buenas prácticas
- Un
api-clientcentralizado: todas las llamadas pasan por un archivo — cambiar base URL o auth se hace en un lugar. - Loading y error son parte del contrato UI: diseña los tres estados (cargando / error / datos) — no es opcional en producción.
Producción (prod) es el entorno real: donde están los usuarios, los datos que importan y las consecuencias. Todo lo demás — local, staging — existe para que los errores ocurran antes de llegar ahí.
- Interceptors para auth, no token pegado en cada llamada.
- Type the response:
fetch<Task[]>()o validación — el JSON que llega no está garantizado (eso lo resuelve el backend con el contrato, Sección II).
5.7 Ejercicio
- Escribe
getTasks()con fetch que lance error si!res.ok— y pruébalo contrahttp://localhost:8000/taskscuando corras la API del libro. - Haz el POST de crear tarea con fetch y verifica que aparece en la lista.
Un array es una colección ordenada de elementos accedidos por posición: ["a","b","c"][0] es "a" (se cuenta desde 0). Python las llama listas, Go slices — misma idea, distinto acento.
localhost significa “esta misma máquina” — es la dirección que tu computadora usa para hablarse a sí misma. Cuando desarrollas, el “servidor” y el “cliente” viven en tu laptop: por eso todo es localhost:3000.
5.8 Mini reto
Tu useEffect + fetch se ejecuta dos veces en desarrollo. ¿Por qué (React StrictMode) y cómo lo resuelves bien — con cleanup o con React Query?
Ejercicio 1.
async function getTasks() {
const res = await fetch("http://localhost:8000/tasks");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}Ejercicio 2.
await fetch("http://localhost:8000/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "nueva" }),
});Mini reto. StrictMode monta→desmonta→remonta en dev para detectar efectos sin cleanup. Fix manual: let cancelled = false + cleanup que la pone true. Fix real: React Query maneja dedupe/cancelación por ti — otra razón por la que las apps serias no hacen fetch en useEffect a mano.
5.9 Vocabulario técnico del capítulo
Una variable es una caja con nombre donde guardas un valor: let total = 42 guarda el 42 bajo el nombre total. Puedes leerla y cambiarla después (total = 50). const = caja que no se puede reemplazar.
Una función es una receta reutilizable: recibe ingredientes (parámetros), hace pasos y devuelve un plato (return). La escribes una vez y la llamas mil veces: add(2, 3) → 5.
Un bucle repite una acción por cada elemento o hasta cumplir una condición: for task in tasks hace algo con cada tarea. map y filter son bucles disfrazados de funciones — transforman/filtran colecciones sin for explícito.
Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.
return hace dos cosas a la vez: devuelve el resultado Y termina la función — lo que esté debajo nunca corre. return task = “aquí está el plato, salgo de la cocina”.
Un diccionario (dict en Python, map en Go, objeto en JS) guarda parejas clave→valor: {"ana": 30}. Es la estructura que más aparece en JSON — un objeto JS es un diccionario.
Un evento es “algo que pasó” convertido en dato: el click del usuario, el payment.succeeded del webhook, el mensaje del socket. La programación moderna es reaccionar a eventos más que seguir un guion.
Una dependencia es código de terceros que tu proyecto usa: npm install, pip install, go get las traen. Cada una es deuda — ahora funciona, pero hay que mantenerla, actualizarla y confiar en ella.
Una query es la pregunta que le haces a la base de datos en SQL: SELECT * FROM tasks WHERE done = false. La BD traduce la pregunta a un plan de búsqueda — por eso los índices importan.
Una cookie es un dato que el servidor pone en tu navegador y el navegador devuelve en cada request. httpOnly = JavaScript no puede leerla (el XSS no la roba); Secure = solo viaja por HTTPS.
Un token es una credencial portable: una cadena que dice quién eres y hasta cuándo. El servidor la emite tras el login; el cliente la presenta en cada request en vez de la contraseña.
Un middleware es un filtro en la cadena del request: pasa por él antes de llegar a tu handler. Auth es middleware — “verifica el token” vive una vez y protege todas las rutas, no se copia en cada una.
Un retry es reintentar lo que falló — las redes fallan, es normal. Backoff = esperar más cada vez (1s, 2s, 4s…): evita martillar a un servicio que ya está sufriendo.