Skip to content

Instantly share code, notes, and snippets.

@wh01s17
Created April 28, 2026 20:24
Show Gist options
  • Select an option

  • Save wh01s17/82e621d4abb1106dec5e71803ccbb0d7 to your computer and use it in GitHub Desktop.

Select an option

Save wh01s17/82e621d4abb1106dec5e71803ccbb0d7 to your computer and use it in GitHub Desktop.
Ejercicios - APIs REST con Fetch

Ejercicios - APIs REST con Fetch

Cada solución debe incluir un pequeño index.html con los botones y un contenedor (<section>, <div> o <ul>) donde renderizar los resultados, y un main.js con las funciones, los addEventListener y las llamadas a fetch. Usa siempre textContent y createElement para escribir en el DOM (nunca innerHTML con strings concatenados). Cuando un ejercicio pida observar la respuesta cruda, abre la consola del navegador (F12) antes de hacer clic.

La API que vamos a usar en casi todos los ejercicios es la Studio Ghibli API en https://ghibliapi.vercel.app/films, que devuelve un arreglo de objetos con campos como id, title, original_title, description, director, producer, release_date y running_time.

Ejercicio 1 - Tu primer fetch

Una API REST es un puente entre dos sistemas: tu navegador (el cliente) le pide datos a un servidor mediante una URL llamada endpoint, y el servidor responde con un paquete de datos en formato JSON. La función nativa moderna para hacer esa petición es fetch(url): delega el trabajo de red al navegador (no congela la pantalla) y devuelve una Promesa que se resolverá cuando llegue la respuesta.

Crea una página con un botón "Ver respuesta cruda". Al hacer clic, llama a fetch("https://ghibliapi.vercel.app/films") y encadena un .then(response => console.log(response)). Observa en la consola que lo que llega no es todavía la lista de películas: es un objeto Response con propiedades como ok, status y headers. El cuerpo de los datos está dentro y todavía hay que extraerlo.

💡 Pista: todavía no uses .json(); el objetivo es ver que fetch devuelve un Response, no los datos directamente. Fíjate especialmente en response.ok (booleano) y response.status (número como 200): los necesitarás más adelante para detectar errores.

Ejercicio 2 - Parseando la respuesta con .json()

El objeto Response que devuelve fetch trae el cuerpo como un flujo de red crudo. Para convertirlo en un objeto JavaScript usable llamamos a response.json(), que también devuelve una promesa (por eso suelen verse dos .then() encadenados): el primero saca el Response, el segundo recibe los datos ya parseados.

Crea un botón "Listar títulos en consola" que haga fetch al endpoint, encadene .then(res => res.json()) y luego .then(films => { ... }). Dentro del segundo .then, recorre el arreglo con for ... of e imprime en consola el formato "<título> - <director>" por cada película (donde <título> y <director> son los valores reales de cada campo, no literales). Agrega también un .catch(err => console.error(err)) al final.

💡 Pista: films ya es un arreglo de objetos JS, así que dentro del for puedes hacer console.log(\${film.title} - ${film.director}`). El doble .then()` es el patrón clásico: el primero pide el JSON, el segundo lo recibe.

Ejercicio 3 - Renderizando en el DOM de forma segura

Las APIs devuelven arreglos de objetos y nuestra UI tiene que mostrarlos en pantalla. La forma segura de hacerlo es construir nodos en memoria con document.createElement, asignar el texto con .textContent y anclarlos al documento con appendChild. Esto evita ataques de inyección (XSS) que ocurren al concatenar strings dentro de innerHTML.

Programa un botón "Mostrar título y año". Al hacer clic, haz fetch al endpoint, parsea el JSON y por cada película crea un <li> con el texto en formato "<título> (<año>)" (sustituyendo por los valores reales de film.title y film.release_date) y agrégalo a un <ul id="lista"> del HTML. Antes de renderizar, vacía la lista con lista.textContent = "" para que un segundo clic no duplique los datos.

💡 Pista: el año ya viene como string en film.release_date. El patrón es siempre el mismo: const li = document.createElement("li"); li.textContent = ...; lista.appendChild(li);. Nunca uses lista.innerHTML += ... con datos que vienen de una API.

Ejercicio 4 - Validar response.ok y atrapar errores

Una trampa importante de fetch es que no rechaza la promesa cuando el servidor responde con un error HTTP (404, 500, etc.): el .then() igual se ejecuta. Por eso es obligatorio validar response.ok y, si es false, lanzar un error con throw new Error(...) para que el flujo salte al .catch(). Solo los errores de red (sin internet, DNS caído) caen al .catch por sí solos.

Programa un botón "Probar URL inválida" que haga fetch a una URL claramente errónea como https://ghibliapi.vercel.app/peliculas-que-no-existen. En el primer .then(res => ...) valida con if (!res.ok) throw new Error(\Error HTTP: ${res.status}`)y solo si pasa, devuelveres.json(). Conecta un .catch(err => ...)que muestre el mensaje de error en un

del DOM. Prueba también con la URL correcta y comprueba que ahí no salta elcatch`.

💡 Pista: sin la validación de res.ok, el .json() podría intentar parsear una página HTML de error y fallar de una forma confusa. La regla de oro: siempre valida res.ok antes de seguir. El throw dentro de un .then lo intercepta el .catch siguiente automáticamente.

Ejercicio 5 - Reescribiendo con async / await

async / await es azúcar sintáctico sobre las promesas: hace que el código asíncrono se lea de arriba a abajo, como si fuera síncrono. Una función marcada con async siempre devuelve una promesa, y dentro podemos usar await para esperar a otra promesa sin bloquear el hilo. Los errores se manejan con try / catch clásico, lo que evita anidar .then().then().catch().

Reescribe los ejercicios 3 y 4 combinados (render seguro + validación de errores) usando async / await. Crea una función async function mostrarPeliculas() que haga const res = await fetch(url), valide if (!res.ok) throw new Error(...), haga const films = await res.json() y luego renderice los <li> en la lista. Envuelve todo en un try / catch y, en el catch, escribe el error en <p id="estado">. Conecta la función a un botón "Cargar (async)".

💡 Pista: la función debe declararse con async para poder usar await dentro. Compara visualmente este código con el de los ejercicios 3 y 4 y observa cómo desaparece la "escalera" de .then() y el .catch queda reemplazado por un try / catch clásico. La validación if (!res.ok) throw ... sigue siendo obligatoria.

Ejercicio 6 - Separando red y UI

Una buena práctica clave es separar responsabilidades: una función que solo habla con la red (hace fetch, valida, parsea y devuelve datos) y otra función que solo dibuja en pantalla (recibe datos y los pinta en el DOM). Esto hace el código más fácil de probar y de cambiar: si mañana la API cambia, solo tocas la función de red; si rediseñas la UI, solo tocas la de render.

Programa dos funciones separadas: async function obtenerPeliculas() que devuelva el arreglo de películas (con validación de res.ok), y function renderPeliculas(peliculas, contenedor) que reciba el arreglo y dibuje un <article> por película con su título y descripción. Conecta un botón "Mostrar título y descripción" que llame a la primera, espere su resultado con await y se lo pase a la segunda. Maneja el error con try / catch en el handler del botón y muestra el mensaje en el mismo <p id="estado"> del ejercicio 4.

💡 Pista: la función de red no toca el DOM y la función de render no llama a fetch. El handler del botón es el que orquesta: try { const data = await obtenerPeliculas(); renderPeliculas(data, contenedor); } catch (e) { estado.textContent = e.message; }. Esta es la arquitectura que vas a reusar en los ejercicios siguientes.

Ejercicio 7 - Caché local: pedir solo la primera vez

Cada fetch cuesta tiempo y consume datos del usuario. Si la información no cambia durante la sesión, podemos cachearla: la primera vez vamos al servidor y guardamos el resultado en una variable; las veces siguientes leemos esa variable y nos ahorramos la petición. Es el patrón más simple de memoización aplicado a llamadas de red.

Toma el ejercicio 6 y agrégale caché. Declara una variable let cachePeliculas = null en el ámbito del módulo. Dentro de obtenerPeliculas(), si cachePeliculas ya tiene datos, devuélvelos directamente sin llamar a fetch; si está en null, haz la petición, valida res.ok antes de tocar el caché y solo si la respuesta es válida guarda el resultado en cachePeliculas y devuélvelo. Para comprobar que funciona, abre la pestaña Network del navegador (F12 → Red) y verifica que solo el primer clic dispara una petición HTTP; los siguientes no.

💡 Pista: la variable de caché vive fuera de la función para que sobreviva entre llamadas. Orden importa: primero validás res.ok (y lanzás el error si falla), recién después asignás a cachePeliculas. Si invertís el orden y la petición falla, contaminás el caché con basura y los clics siguientes devuelven datos corruptos sin volver a pedir. El cuerpo de la función queda así: if (cachePeliculas) return cachePeliculas; const res = await fetch(...); if (!res.ok) throw new Error(...); cachePeliculas = await res.json(); return cachePeliculas;.

Ejercicio 8 - Encapsulando todo en una clase

Cuando una API tiene varios métodos relacionados (listar, filtrar, buscar por id), conviene agruparlos en una clase. La clase guarda en this la URL base y el caché, y expone métodos async que el resto de la app puede usar sin saber cómo está implementada por dentro. Es la misma idea de "separar responsabilidades" del ejercicio 6, pero llevada un paso más allá.

Crea una clase GhibliAPI con:

  • Un constructor() que guarde this.url = "https://ghibliapi.vercel.app/films" y this.cache = null.
  • Un método async obtenerPeliculas() que use el caché del ejercicio 7 (ahora guardado en this.cache en lugar de una variable de módulo).
  • Un método async listarTitulosYDirector(contenedor) que renderice "<título> - <director>" por cada película.
  • Un método async listarTitulosYAnio(contenedor) que renderice "<título> (<año>)".
  • Un método async listarTitulosYDescripcion(contenedor).
  • Un método async listarConDuracion(contenedor) que renderice "<título> - <running_time> min" (el campo running_time viene como string con los minutos).

En el HTML agrega un botón por cada método y, en el script, instancia const api = new GhibliAPI() y conecta cada botón a su método correspondiente.

💡 Pista: los métodos de la clase llaman internamente a this.obtenerPeliculas(), así todos comparten el mismo caché. Recuerda que dentro de la clase los métodos async se declaran sin la palabra function: async listarTitulosYDirector(contenedor) { ... }. Antes de renderizar, vacía el contenedor con contenedor.textContent = "" (igual que en el ejercicio 3) para que clics repetidos o cambios de método no apilen resultados anteriores.

Ejercicio 9 - Filtrando por año con prompt

Las APIs no siempre tienen un endpoint exacto para lo que necesitamos: a veces conviene traer todo y filtrar en el cliente. Para conjuntos pequeños (decenas de elementos) esto es perfectamente razonable y nos permite reusar el caché que ya construimos. El método Array.prototype.filter recibe una función que devuelve true para los elementos que queremos conservar.

Agrega a la clase GhibliAPI un método async buscarPorAnio(contenedor) que use prompt("Ingresa un año") para pedir un año al usuario, llame a this.obtenerPeliculas(), filtre el arreglo con peliculas.filter(p => p.release_date === anio) y renderice los títulos resultantes. Si no hay coincidencias, muestra el texto "No hay películas del año X" en el contenedor. Conéctalo a un botón "Buscar por año". Prueba con 2001 (debería aparecer "Spirited Away") y con 1900 (no debería haber resultados).

💡 Pista: prompt devuelve un string (o null si el usuario cancela). Como release_date también es un string, puedes comparar directamente con ===. Valida primero que el usuario no haya cancelado: if (anio === null) return;. Vacía el contenedor antes de renderizar para no apilar resultados de búsquedas anteriores. Nota: usamos prompt solo para no agregar más HTML; en una app real reemplazaríamos eso por un <input type="number"> y un botón. La lógica de filtrado sería idéntica.

Ejercicio 10 - Múltiples peticiones en paralelo con Promise.all

Cuando necesitas datos de varios endpoints independientes, lanzarlos uno después de otro con await es lento: cada petición espera a que termine la anterior. Promise.all([p1, p2, p3]) recibe un arreglo de promesas, las dispara en paralelo y resuelve cuando todas terminan, devolviendo un arreglo con los resultados en el mismo orden. Si una sola falla, Promise.all se rechaza inmediatamente.

Combina dos APIs públicas usando Promise.all: la de Ghibli (https://ghibliapi.vercel.app/films) y PokeAPI (https://pokeapi.co/api/v2/pokemon/pikachu). Crea una función async function cargarTodo() que dispare ambas peticiones en paralelo, espere los resultados con const [films, pikachu] = await Promise.all([...]) y renderice en pantalla la cantidad de películas de Ghibli y el nombre del Pokémon. Envuelve todo en try / catch y conéctalo a un botón "Cargar en paralelo". Mide en la pestaña Network cuánto tarda con Promise.all y compáralo con hacer dos await consecutivos.

💡 Pista: cada elemento del arreglo de Promise.all debe ser una promesa que ya devuelva los datos parseados, no el Response crudo. Crea una pequeña función async function getJSON(url) que haga fetch, valide res.ok y devuelva res.json(), y reúsala: Promise.all([getJSON(urlGhibli), getJSON(urlPoke)]). Fíjate que este helper es exactamente el patrón que venimos repitiendo desde el ejercicio 4: validar res.ok, parsear JSON, devolver datos. En un proyecto real, lo extraerías a un módulo desde el principio y todos los ejercicios anteriores lo reutilizarían. Para sentir la diferencia entre serial y paralelo, cambia temporalmente una URL por una inválida y observa cómo Promise.all salta al catch apenas falla una.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment