Ir al contenido
wannacode

Datos desde una API

En resumen: fetch obtiene datos de una API con async/await, comprueba response.ok y lee el JSON, y la página muestra uno de cuatro estados: cargando, error, vacío o datos.

Problema

Un escaparate vacío

La página de lácteos toma sus productos del servidor. Hoy el servidor tiene un problema y responde con el error 500. La compradora ve un encabezado y debajo no hay nada.

HTML, CSS, JS

La consola dice products.map is not a function. El código tomó la respuesta del error, { "error": "…" }, por una lista de productos. La compradora no ve ningún error ni un «Intentar de nuevo». Y aunque el servidor funcione, la página está en blanco mientras la respuesta viene en camino.

La idea

fetch en la página

fetch(address) envía una petición y devuelve una promesa (Promise) de la respuesta. Con await la función espera la respuesta sin detener la página:

const response = await fetch('/api/products?category=4');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
  • fetch solo lanza un error cuando no hay respuesta: no hay red. Una respuesta con 404 o 500 es una respuesta, y la promesa se cumple.
  • response.ok es verdadero solo para los códigos de 200 a 299. Comprobarlo es cosa tuya.
  • response.json() lee el cuerpo y lo convierte en un objeto. Es también una promesa, así que también lleva await.

En el sandbox las direcciones /api/… las responde la API simulada de la tienda, sin red: GET /api/products (con ?q= y ?category=), /api/products/3, /api/categories, /api/orders y POST /api/orders. Una lección puede hacerla lenta, indicarle que responda con 500 o hacer que devuelva una lista vacía. Así cada estado es real y siempre el mismo.

La idea

Cuatro estados

Una petición tarda y tiene un resultado. La pantalla tiene que decir qué pasa en cada momento:

  • cargando: la petición salió, todavía no hay respuesta;
  • datos: llegó una lista de productos;
  • vacío: llegó una lista, pero está vacía. No es un error;
  • error: no hay respuesta, o la respuesta no es ok.

Cada estado es un elemento en el HTML. El código muestra y oculta los elementos con la propiedad hidden:

<p id="loading" role="status">Cargando…</p>
<p id="error" role="alert" hidden></p>
<p id="empty" hidden>No hay productos en esta categoría.</p>
<ul id="products"></ul>

«Cargando…» se ve desde el principio, antes incluso de que se ejecute JavaScript. role="status" hace que un lector de pantalla lo lea, y role="alert" lee el error en cuanto aparece.

Míralo

Observa una petición

async function load() {
try {
const response = await fetch('/api/products?category=4');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
empty.hidden = products.length > 0;
list.replaceChildren(...products.map(productItem));
} catch {
error.textContent = 'Los productos no se cargaron.';
error.hidden = false;
} finally {
loading.hidden = true;
}
}
Paso a paso, en palabras
  1. load() envía la petición. Todavía no hay respuesta: la pantalla muestra «Cargando…» desde el HTML.
  2. await espera, pero la página no se congela: la compradora puede hacer scroll y el spinner gira. Con una red lenta esto son segundos.
  3. 200 y un array: ok es true, json() da los productos, se dibujan las líneas y finally oculta «Cargando…».
  4. La misma petición, respuesta 500. fetch no lanza nada: recibió una respuesta. Lanza nuestra comprobación de ok, y catch muestra el error.
  5. 200 y []: esto no es un error. El array está vacío, así que la página dice «no hay productos» en lugar de dejar un espacio en blanco.
  6. Cuatro estados, cada uno visible: cargando, datos, error, vacío. finally oculta «Cargando…» en cada salida de try.

El mismo código, tres respuestas distintas. Con 500, fetch no lanza nada: lanza la comprobación de ok, y catch muestra el error. finally oculta «Cargando…» en los tres casos.

Míralo

La API lenta

Aquí la API responde al cabo de 1,2 segundos. Pulsa «Ejecutar» y observa «Cargando…».

HTML, CSS, JS

Todo el código de carga:

async function load() {
try {
const response = await fetch('/api/products?category=4');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
empty.hidden = products.length > 0;
list.replaceChildren(...products.map(productItem));
} catch {
error.textContent = 'Los productos no se cargaron.';
error.hidden = false;
} finally {
loading.hidden = true;
}
}
load();

Pruébalo

Error

Aquí la primera petición recibe un 500 y la segunda pasa. Cuando haya un error, haz que la página muestre «Los productos no se cargaron.» en #error y el botón «Intentar de nuevo», que «Cargando…» desaparezca y que no se muestre «no hay productos». El botón ya vuelve a cargar: después de pulsarlo, los productos tienen que estar en la página.

HTML, CSS, JS

Pruébalo

Vacío

Aquí la API devuelve una lista vacía. Muestra «No hay productos en esta categoría.» cuando haya cero productos, y ocúltalo cuando haya alguno.

HTML, CSS, JS

Pruébalo

Encuentra el error

El servidor responde con 200 y productos, y la página muestra un error.

Elige la línea con el error.

La idea

Enviar: el botón y la espera

El pedido sale con POST. Los datos van en el cuerpo como JSON, y la cabecera Content-Type le dice al servidor qué recibe:

const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...data, items }),
});

data es el objeto con los campos del formulario: Object.fromEntries(new FormData(form)) convierte los pares nombre=valor en un objeto { name: …, email: …, address: … }.

Mientras la respuesta viene en camino, el botón «Pedir» sigue activado. Una compradora impaciente lo pulsa dos veces:

HTML, CSS, JS

Dos peticiones, dos pedidos: 61 y 62. La página muestra el último, y nadie se da cuenta de que el primero también se aceptó.

Pruébalo

Un clic, un pedido

Desactiva el botón «Pedir» antes de que salga la petición y vuelve a activarlo cuando llegue la respuesta, aunque sea un error. La comprobación pulsa dos veces seguidas y espera un solo pedido: el 61.

HTML, CSS, JS
Más precisamente, para curiosos

Un error de red. Sin red, fetch lanza TypeError: Failed to fetch. catch atrapa eso y tu propio throw en !response.ok: para la compradora, los dos significan «no cargó».

El cuerpo de un error. Un servidor suele explicar el error en el cuerpo, como JSON. Con 400, la API simulada devuelve { "errors": { "email": "…" } }: un error por campo. Por eso el ejercicio lee el cuerpo también cuando hay un error.

Intentar de nuevo. Un botón «Intentar de nuevo» junto al error solo vuelve a llamar a load(). Antes, «Cargando…» tiene que volver a mostrarse y el error tiene que ocultarse.

Una carrera entre peticiones. Si la compradora cambia de categoría dos veces seguidas, las dos respuestas pueden llegar en orden inverso y la página muestra la categoría vieja. Cómo detener una petición vieja (AbortController) está en el módulo sobre efectos.

Comprobaciones en el navegador y en el servidor. required y type="email" detienen el formulario antes de que salga. El servidor vuelve a comprobar, porque una petición puede llegar sin nuestra página. En el ejercicio los campos no tienen comprobaciones integradas, para que veas lo que responde el servidor.

Ejercicio

El pedido

Envía el pedido: POST /api/orders con name, email y address del formulario e items del array. Mientras se envía, el botón está desactivado. Con 400, pon cada mensaje de errors en el párrafo bajo su campo (#email-error y así sucesivamente) y borra los mensajes viejos en cada envío nuevo. Si sale bien, escribe «Pedido 61 aceptado. Total: 4,00 €» en #result (el número y el total vienen de la respuesta).

HTML, CSS, JS

Test

Ponte a prueba

1. El servidor responde con 500. ¿Qué hace await fetch(…)?
2. La API devuelve []. ¿Qué condición muestra «no hay productos»?
3. ¿Por qué el botón «Pedir» se desactiva antes de que salga la petición y no después?
4. ¿Por qué «Cargando…» se oculta en finally?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·