5  F5. Hablar con una API desde el frontend

fetch, axios, React Query y HttpClient — el cliente del contrato

NotaEn una frase

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

  • fetch sin revisar res.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-client centralizado: 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

  1. Escribe getTasks() con fetch que lance error si !res.ok — y pruébalo contra http://localhost:8000/tasks cuando corras la API del libro.
  2. 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.

5.10 Lo que deberías saber hacer ahora