Saltar al contenido
schedule 25 min JavaScript

Ejercicio final: app interactiva con API y WebMCP

Has recorrido toda la sección de JavaScript: variables, funciones, el DOM, eventos, asincronía, arrays, objetos, módulos, manejo de errores y WebMCP imperativo. Ahora toca demostrar que puedes combinar todo en un proyecto real.

Vas a construir un Pokémon Explorer: una aplicación que consulta la PokéAPI, renderiza resultados en el DOM, responde a interacciones del usuario y expone herramientas WebMCP para que un agente de IA también pueda buscar Pokémon.

El proyecto: Pokémon Explorer

La PokéAPI es una API gratuita y sin autenticación con datos de más de 1000 Pokémon. Tu app debe permitir buscarlos, ver sus detalles y, opcionalmente, guardar favoritos. Todo con vanilla JavaScript — sin frameworks, sin librerías.

El endpoint principal que usarás es:

api.js
// Buscar un Pokémon por nombre o ID
const response = await fetch("https://pokeapi.co/api/v2/pokemon/pikachu");
const pokemon = await response.json();

// Datos útiles del response:
// pokemon.name          → "pikachu"
// pokemon.id            → 25
// pokemon.sprites.front_default → URL de la imagen
// pokemon.types         → [{ type: { name: "electric" } }]
// pokemon.stats         → [{ base_stat: 35, stat: { name: "hp" } }, ...]
// pokemon.height        → 4 (en decímetros)
// pokemon.weight        → 60 (en hectogramos)

Requisitos obligatorios

Tu app debe cumplir todos estos requisitos. Cada uno pone en práctica algo que has aprendido en las lecciones anteriores.

1. Barra de búsqueda con event handling

  • Un <input> de búsqueda y un botón para buscar
  • Que funcione tanto al hacer clic en el botón como al pulsar Enter en el input
  • Que no haga nada si el input está vacío (validación básica)
  • Que convierta la búsqueda a minúsculas antes de enviarla (la PokéAPI solo acepta minúsculas)

2. Fetch a la PokéAPI y renderizado

  • Usa fetch() con async/await para obtener datos del Pokémon
  • Muestra en el DOM: imagen (sprites.front_default), nombre, número, tipos, y al menos 3 stats (HP, ataque, defensa)
  • Usa destructuring para extraer los datos del response
  • Usa template literals para construir el HTML

3. Manejo de errores

  • Envuelve las peticiones en try/catch
  • Si el Pokémon no existe (status 404), muestra un mensaje amigable: "No se encontró ningún Pokémon con ese nombre"
  • Si hay un error de red, muestra: "Error de conexión. Revisa tu internet e inténtalo de nuevo"
  • Nunca muestres un error críptico al usuario

4. Estado de carga

  • Mientras la petición está en curso, muestra un indicador de carga (puede ser texto "Buscando..." o un spinner CSS)
  • Deshabilita el botón de búsqueda mientras se carga para evitar peticiones duplicadas
  • Oculta el indicador de carga cuando la petición termine (tanto en éxito como en error)

5. JavaScript moderno

  • Usa const y let — nunca var
  • Usa arrow functions para callbacks y handlers
  • Usa destructuring al extraer datos del JSON
  • Usa template literals para construir strings con datos dinámicos
  • Usa optional chaining (?.) donde tenga sentido (por ejemplo, pokemon.sprites?.front_default)

6. WebMCP: herramienta para agentes de IA

  • Registra al menos una herramienta con navigator.modelContext.registerTool()
  • La herramienta debe llamarse "buscar-pokemon" y aceptar un parámetro nombre (string)
  • El handler debe hacer fetch a la PokéAPI y devolver los datos del Pokémon (o un error si no existe)
  • Comprueba que navigator.modelContext existe antes de registrar

Plantilla HTML de partida

Puedes usar esta estructura como punto de partida. Cópiala y trabaja sobre ella:

index.html
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pokémon Explorer</title>
    <style>
        /* Estilos base — personaliza como quieras */
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: system-ui, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 2rem;
            background: #1a1a2e;
            color: #eee;
        }
        h1 { text-align: center; margin-bottom: 2rem; }

        .search-bar {
            display: flex;
            gap: 0.5rem;
            margin-bottom: 2rem;
        }
        .search-bar input {
            flex: 1;
            padding: 0.75rem 1rem;
            border: 2px solid #333;
            border-radius: 8px;
            background: #16213e;
            color: #eee;
            font-size: 1rem;
        }
        .search-bar input:focus {
            outline: none;
            border-color: #F7DF1E;
        }
        .search-bar button {
            padding: 0.75rem 1.5rem;
            background: #F7DF1E;
            color: #1a1a2e;
            border: none;
            border-radius: 8px;
            font-weight: bold;
            cursor: pointer;
        }
        .search-bar button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }

        .loading { text-align: center; color: #F7DF1E; padding: 2rem; }
        .error { text-align: center; color: #e74c3c; padding: 2rem; }

        .pokemon-card {
            background: #16213e;
            border-radius: 16px;
            padding: 2rem;
            text-align: center;
        }
        .pokemon-card img { width: 200px; height: 200px; }
        .pokemon-card h2 { text-transform: capitalize; margin: 1rem 0 0.5rem; }

        .pokemon-types { display: flex; gap: 0.5rem; justify-content: center; margin: 1rem 0; }
        .pokemon-types span {
            padding: 0.25rem 0.75rem;
            border-radius: 20px;
            font-size: 0.875rem;
            background: #333;
        }

        .pokemon-stats { text-align: left; margin-top: 1.5rem; }
        .stat-bar {
            display: flex;
            align-items: center;
            gap: 0.5rem;
            margin: 0.5rem 0;
        }
        .stat-bar span:first-child {
            width: 120px;
            text-transform: capitalize;
            font-size: 0.875rem;
        }
        .stat-bar .bar {
            flex: 1;
            height: 8px;
            background: #333;
            border-radius: 4px;
            overflow: hidden;
        }
        .stat-bar .bar-fill {
            height: 100%;
            background: #F7DF1E;
            border-radius: 4px;
        }
    </style>
</head>
<body>
    <h1>Pokémon Explorer</h1>

    <div class="search-bar">
        <input type="text" id="search-input" placeholder="Busca un Pokémon (ej: pikachu, 25)">
        <button id="search-btn">Buscar</button>
    </div>

    <div id="result"></div>

    <script src="app.js"></script>
</body>
</html>

Guía de implementación

No tienes que seguir este orden exacto, pero te puede ayudar a no perderte:

  1. Empieza por la búsqueda básica: captura el evento del botón, lee el valor del input, haz un console.log para verificar.
  2. Añade el fetch: haz la petición a la PokéAPI y muestra los datos en consola.
  3. Renderiza en el DOM: construye el HTML con template literals e insértalo en #result.
  4. Añade el manejo de errores: prueba buscando "asdfgh" (debería dar 404) y desconecta internet (debería dar error de red).
  5. Añade el estado de carga: muestra "Buscando..." y deshabilita el botón.
  6. Refina con JS moderno: refactoriza para usar destructuring, optional chaining, etc.
  7. Registra la herramienta WebMCP: al final, añade el registerTool().

Ejemplo de cómo podrían verse las funciones clave

No te voy a dar la solución completa (sería trampa), pero sí la estructura de las funciones principales para que sepas hacia dónde ir:

app.js
// Referencias al DOM
const searchInput = document.querySelector("#search-input");
const searchBtn = document.querySelector("#search-btn");
const resultDiv = document.querySelector("#result");

// Función principal de búsqueda
const buscarPokemon = async (nombre) => {
    // 1. Validar que el nombre no esté vacío
    // 2. Mostrar estado de carga
    // 3. Hacer fetch a la PokéAPI
    // 4. Si ok → extraer datos con destructuring → renderizar
    // 5. Si 404 → mostrar mensaje "no encontrado"
    // 6. Si error de red → mostrar mensaje de error
    // 7. Restaurar el botón de búsqueda
};

// Función para renderizar un Pokémon en el DOM
const renderPokemon = ({ name, id, sprites, types, stats }) => {
    // Construir HTML con template literals
    // Insertar en resultDiv
};

// Función para mostrar errores
const mostrarError = (mensaje) => {
    resultDiv.innerHTML = `<div class="error">${mensaje}</div>`;
};

// Event listeners
searchBtn.addEventListener("click", () => {
    const nombre = searchInput.value.trim().toLowerCase();
    buscarPokemon(nombre);
});

searchInput.addEventListener("keydown", (e) => {
    if (e.key === "Enter") {
        const nombre = searchInput.value.trim().toLowerCase();
        buscarPokemon(nombre);
    }
});

// WebMCP (si está disponible)
if ("modelContext" in navigator) {
    navigator.modelContext.registerTool({
        name: "buscar-pokemon",
        // ... definición completa
    });
}

Bonus: desafíos extra

Si terminas los requisitos obligatorios y quieres más, aquí van desafíos adicionales. Cada uno trabaja un concepto diferente:

Guardar favoritos en localStorage

  • Añade un botón "Guardar en favoritos" en la card del Pokémon
  • Almacena los favoritos en localStorage como un array JSON
  • Muestra una sección de favoritos que se mantenga al recargar la página
  • Permite eliminar un Pokémon de favoritos

Comparar dos Pokémon

  • Añade un modo "comparar" que permita buscar dos Pokémon y mostrarlos lado a lado
  • Resalta en cada stat cuál de los dos tiene el valor más alto
  • Muestra quién ganaría según la suma total de stats

Fetch múltiple con Promise.allSettled()

  • Añade un botón "Equipo aleatorio" que genere 6 IDs aleatorios (entre 1 y 1010)
  • Usa Promise.allSettled() para hacer las 6 peticiones en paralelo
  • Muestra los Pokémon que se cargaron correctamente e indica si alguno falló
  • Muestra un indicador de progreso mientras cargan
equipo-aleatorio.js
// Pista para el equipo aleatorio
const generarEquipo = async () => {
    const ids = Array.from({ length: 6 }, () =>
        Math.floor(Math.random() * 1010) + 1
    );

    const promesas = ids.map(id =>
        fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(r => r.json())
    );

    const resultados = await Promise.allSettled(promesas);

    const exitosos = resultados
        .filter(r => r.status === "fulfilled")
        .map(r => r.value);

    const fallidos = resultados
        .filter(r => r.status === "rejected")
        .length;

    // Renderizar exitosos y mostrar cuántos fallaron
};

Registrar herramienta WebMCP avanzada

  • Registra una segunda herramienta "comparar-pokemon" que reciba dos nombres y devuelva una comparación
  • O una herramienta "equipo-pokemon" que genere un equipo aleatorio y devuelva los datos

Checklist final

Antes de darte por satisfecho, verifica:

  • ¿Puedes buscar "pikachu" y ver su imagen, nombre, tipos y stats?
  • ¿Puedes buscar "25" (por ID) y funciona igual?
  • ¿Buscar "asdfgh" muestra un mensaje de error amigable?
  • ¿Se muestra un indicador de carga mientras busca?
  • ¿El botón se deshabilita durante la carga?
  • ¿Funciona pulsar Enter en el input?
  • ¿Si el input está vacío, no hace nada al buscar?
  • ¿No hay errores en la consola del navegador?
  • ¿Usaste const/let, arrow functions, destructuring y template literals?
  • ¿La herramienta WebMCP está registrada (comprueba con "modelContext" in navigator)?

Recurso adicional

Si quieres profundizar aún más, también tienes disponible el curso en vídeo gratuito de JavaScript: https://www.cursosdesarrolloweb.es/course/curso-de-javascript-aprendiendo-las-bases

code

Pokémon Explorer: app interactiva con API y WebMCP

Difícil schedule 60-90 min

Construye el Pokémon Explorer completo siguiendo todos los requisitos obligatorios descritos arriba: búsqueda con eventos, fetch a la PokéAPI, renderizado en el DOM, manejo de errores, estado de carga, JavaScript moderno y al menos una herramienta WebMCP.

Crea dos archivos: index.html (puedes usar la plantilla de arriba) y app.js con toda la lógica.

Extra (opcional):

  • Favoritos guardados en localStorage que persistan al recargar
  • Modo comparar: dos Pokémon lado a lado con stats resaltados
  • Equipo aleatorio con Promise.allSettled()
  • Herramienta WebMCP adicional para comparar o generar equipos
lightbulb Pistas

Divide el problema en funciones pequeñas: una para buscar, una para renderizar, una para mostrar errores, una para el estado de carga. Empieza por hacer que la búsqueda funcione en consola (solo console.log del response), luego renderiza en el DOM, y deja el WebMCP para el final. Si te atascas con los stats, recuerda que pokemon.stats es un array de objetos con base_stat y stat.name — puedes recorrerlo con map().

Has completado la sección de JavaScript. Ya sabes manipular el DOM, responder a eventos, trabajar con APIs asíncronas, manejar errores, usar las características modernas del lenguaje y exponer herramientas a agentes de IA con WebMCP. Tienes las herramientas para crear cualquier interactividad en la web. En la siguiente sección vas a aprender Git para controlar versiones de tu código, colaborar con otros y no perder nunca tu trabajo.

Newsletter

Recibe nuevos cursos, actualizaciones, artículos del blog y promociones en tu correo.