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.
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 quefetchdevuelve unResponse, no los datos directamente. Fíjate especialmente enresponse.ok(booleano) yresponse.status(número como200): los necesitarás más adelante para detectar errores.
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:
filmsya es un arreglo de objetos JS, así que dentro delforpuedes hacerconsole.log(\${film.title} - ${film.director}`). El doble.then()` es el patrón clásico: el primero pide el JSON, el segundo lo recibe.
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 useslista.innerHTML += ...con datos que vienen de una API.
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 validares.okantes de seguir. Elthrowdentro de un.thenlo intercepta el.catchsiguiente automáticamente.
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
asyncpara poder usarawaitdentro. Compara visualmente este código con el de los ejercicios 3 y 4 y observa cómo desaparece la "escalera" de.then()y el.catchqueda reemplazado por untry / catchclásico. La validaciónif (!res.ok) throw ...sigue siendo obligatoria.
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.
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 acachePeliculas. 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;.
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 guardethis.url = "https://ghibliapi.vercel.app/films"ythis.cache = null. - Un método
async obtenerPeliculas()que use el caché del ejercicio 7 (ahora guardado enthis.cacheen 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 camporunning_timeviene 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étodosasyncse declaran sin la palabrafunction:async listarTitulosYDirector(contenedor) { ... }. Antes de renderizar, vacía el contenedor concontenedor.textContent = ""(igual que en el ejercicio 3) para que clics repetidos o cambios de método no apilen resultados anteriores.
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:
promptdevuelve un string (onullsi el usuario cancela). Comorelease_datetambié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: usamospromptsolo 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.
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.alldebe ser una promesa que ya devuelva los datos parseados, no elResponsecrudo. Crea una pequeña funciónasync function getJSON(url)que hagafetch, valideres.oky devuelvares.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: validarres.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ómoPromise.allsalta alcatchapenas falla una.