Construí mi propia Pokédex (sí, con la PokeAPI)

Desde niño me gusta Pokémon. El Halloween pasado me disfracé de Ash Ketchum y, días después, me topé con algo que me llamó la atención: “¿hay una API de Pokémon?” OMFG, sí. Se llama PokéAPI, es gratuita, no pide API key y trae una cantidad absurda de datos. Vi proyectos padrísimos y obvio no me quise quedar atrás. Manos al código.


Primero ¿Qué rayos es un API?

Piensa en un mesero: le pides algo del menú (una URL), él lleva la orden a cocina (el servidor) y regresa con tu platillo en JSON. Tú no entras a la cocina, nomás consumes el resultado. Esa es la idea: una interfaz para pedir y recibir datos de forma ordenada.

Piensa en un mesero: le pides algo del menú (una URL), él va a la cocina (el servidor) y vuelve con tu platillo en JSON. No entras a la cocina, solo consumes el resultado. Eso es una API: una forma ordenada de pedir y recibir datos o acciones entre programas.

Lo esencial en 30 segundos

  • API: puente para que una app hable con otra.
  • Endpoint: dirección específica del menú (/pokemon/25).
  • Método: qué quieres hacer: GET (leer), POST (crear), PUT/PATCH (editar), DELETE (borrar).
  • Parámetros: filtros u opciones (?limit=20).
  • Respuesta: casi siempre JSON.
  • Código HTTP: 200 OK, 404 Not Found, 500 Error.
  • API key: tu “gafete” para identificarte y medir uso.

¿Para qué sirve?

  • Apps y sitios: traer productos, usuarios, imágenes, mapas, clima.
  • Automatización: scripts que consultan/reportan datos.
  • Integraciones: pagos, correos, WhatsApp/Telegram, GitHub.
  • IoT/makers: un Raspberry manda lecturas o pide info.
  • Análisis: consumir datos crudos y graficarlos/guardarlos.

Ahora si vamos al Pokedex

Quería algo rápido y sencillo con lo que ya conozco. Primero hice un script en Python para hacer pruebas… pero me cayó el veinte: quería que las pruebas vivieran aquí mismo en el blog. Para mi, lo más práctico y rápido era volver a mi relación amor-odio con JavaScript, montarlo en un HTML y echarlo a andar en este mismos servidor.

Python
import requests

API = "https://pokeapi.co/api/v2/pokemon/"

def get_pokemon(q="pikachu"):
    url = API + str(q).lower().strip()
    r = requests.get(url, timeout=15)
    if r.status_code != 200:
        print("Pokémon no encontrado 😭")
        return
    data = r.json()

    # Elegir el mejor sprite disponible
    art = (data.get("sprites", {})
              .get("other", {})
              .get("official-artwork", {})
              .get("front_default")) or data["sprites"]["front_default"]

    tipos = [t["type"]["name"].title() for t in data["types"]]
    habilidades = [a["ability"]["name"].title() for a in data["abilities"]]

    print(f"#{data['id']:03d} · {data['name'].title()}")
    print(f"Tipos: {', '.join(tipos)}")
    print(f"Altura: {data['height']/10:.1f} m")
    print(f"Peso: {data['weight']/10:.1f} kg")
    print(f"Habilidades: {', '.join(habilidades)}")
    print(f"Sprite: {art}")

if __name__ == "__main__":
    # Cambia 'pikachu' por un nombre o ID, ej. '25' o 'charizard'
    get_pokemon("pikachu")

El primer intento fue armar la Pokédex sobre una imagen base. Meh. No me encantó. Así que me fui por el camino difícil (y divertido): construirla en CSS, pieza por pieza —carcasa, cámara, luces, pantalla, botones—. Con ayuda de ChatGPT, varios prompts y ajustes manuales, quedó.

Me base en esta imagen que me encontré (quedó masomenos similar)

A partir de aquí viene el esqueleto:

style.css

/* Reset y tipografías base */
*{box-sizing:border-box;margin:0;padding:0}
body{
  background:#d32f2f;min-height:100vh;display:flex;justify-content:center;align-items:center;
  font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial,sans-serif;
}

/* --- Pokédex (marco) --- */
.pokedex{
  position:relative;width:90vw;max-width:400px;aspect-ratio:2/3;
  background:#d32f2f;border:5px solid #333;border-radius:20px;overflow:hidden
}
/* Franja/terminación superior */
.pokedex::before{content:"";position:absolute;top:0;left:0;width:100%;height:12%;border-bottom:5px solid #333}

/* Cámara (círculo azul) y luces */
.camera{
  position:absolute;top:4%;left:10%;width:18%;aspect-ratio:1;background:#40c4ff;
  border:3px solid #333;border-radius:50%;outline:4px solid #fff;outline-offset:-4px
}
.light{position:absolute;top:8%;width:6%;aspect-ratio:1;border:3px solid #333;border-radius:50%}
.light.red{left:35%;background:#f44336}.light.yellow{left:44%;background:#ffeb3b}.light.green{left:53%;background:#4caf50}

/* --- Pantalla (área blanca) --- */
.screen{
  position:absolute;top:18%;left:7%;width:86%;height:42%;background:#eee;border:5px solid #333;border-radius:10px;
  box-shadow:inset -4px -4px 0 rgba(0,0,0,.1);overflow:hidden
}
/* LED rojo pequeño en la pantalla */
.screen::before{content:"";position:absolute;top:6px;left:10px;width:14px;height:14px;background:#f44336;border-radius:50%;outline:3px solid #333}
/* (QUITADO) .screen .button-red */

/* Contenido centrado dentro de la pantalla */
.screen-content{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.25rem;padding:.5rem 1rem;text-align:center}
.sprite{width:65%;max-height:65%;object-fit:contain;image-rendering:pixelated;filter:drop-shadow(0 2px 0 rgba(0,0,0,.15))}
.poke-name{font-size:1.1rem;font-weight:800;color:#333;text-transform:capitalize;text-shadow:0 1px #fff}
.loading,.error{position:absolute;bottom:8px;right:8px;font-size:.8rem;color:#666;background:#fff8;padding:.1rem .4rem;border-radius:4px;display:none}
.error{color:#b71c1c;background:#ffebee}

/* Botones decorativos (rectángulos) */
.small-buttons{position:absolute;bottom:22%;left:12%;width:60%;display:flex;justify-content:space-between}
.small-buttons .btn{flex:1;margin:0 2%;height:4%;background:#00bfa5;border:5px solid #333;border-radius:5px}
.small-buttons .btn.red{background:#f44336;flex:.4}

/* --- Buscador (izq. de flechas) --- */
/* Fondo y z-index para tapar cualquier línea gris detrás */
.search-wrap{
  position:absolute;bottom:26%;left:12%;
  right:calc(10% + 22% + 12px);height:9%;
  display:flex;gap:.4rem;align-items:center;z-index:5;background:#d32f2f;padding-right:.2rem
}
.search-wrap input{
  width:100%;height:100%;border:5px solid #333;border-radius:8px;padding:0 .6rem;font-weight:700;background:#fafafa;outline:none
}
.search-wrap button{
  height:100%;aspect-ratio:1;border-radius:8px;border:5px solid #333;background:#ffd54f;cursor:pointer;display:grid;place-items:center
}

/* --- Flechas (nav) --- */
.nav-buttons{position:absolute;bottom:18%;right:10%;width:22%;height:20%;display:flex;justify-content:space-between;align-items:center}
.nav-btn{position:relative;width:44%;aspect-ratio:1;background:#555;border:5px solid #333;border-radius:50%;display:flex;justify-content:center;align-items:center;cursor:pointer}
/* Triangulitos hechos con bordes */
.nav-btn.left::before,.nav-btn.right::before{content:"";display:block;width:0;height:0;border-top:10px solid transparent;border-bottom:10px solid transparent}
.nav-btn.left::before{border-right:14px solid #333}
.nav-btn.right::before{border-left:14px solid #333}

/* --- Display verde (datos) --- */
.bottom-display{
  position:absolute;bottom:7%;left:12%;width:76%;height:16%;
  background:#00e676;border:5px solid #333;border-radius:8px;
  padding:.35rem .55rem;display:flex;align-items:center;overflow:hidden
}
/* Grid 2 columnas para etiquetas/valores */
.stats{
  list-style:none;width:100%;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.25rem .6rem;
  font-size:.65rem;font-weight:600;line-height:1.15
}
.stats li{display:flex;gap:.4rem;align-items:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.stats li.col-2{grid-column:1/-1}
.stats span{display:inline-block;padding:.05rem .35rem;background:#00c853;border:2px solid #333;border-radius:4px;font-weight:800}

/* --- Responsive (móviles ≤ 600px) --- */
@media (max-width:600px){

  /* Usa 100dvh para ocupar alto real en móviles */
  body{ min-height:100dvh; }

  /* La pokedex ocupa toda la pantalla móvil */
  .pokedex{width:100vw;height:100dvh;max-width:none;aspect-ratio:auto;border-radius:0}
  .pokedex::before{height:10%}

  /* Pantalla blanca un poco más grande en móvil */
  .screen{top:17%;left:6%;width:88%;height:50%}

  /* Buscador más arriba y delgado en móvil */
  .search-wrap{bottom:24%;right:calc(8% + 22% + 10px);left:10%;height:8%}

  /* Flechas y display reubicados */
  .nav-buttons{bottom:17%;right:8%;width:22%;height:22%}
  .bottom-display{bottom:5%;left:10%;width:80%;height:18%}

  /* Texto de stats un poco más chico para que quepa */
  .stats{font-size:.7rem}
}

/* --- Blink de la cámara al buscar --- */
@keyframes cameraBlink {
  0%,100% { background:#40c4ff; }   /* azul claro actual */
  50%     { background:#1e88e5; }   /* azul más oscuro */
}
.camera.blink {
  animation: cameraBlink .8s ease-in-out infinite;
}

index.html

HTML
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8" />
  <title>Mini Pokédex</title>
  <link rel="stylesheet" href="style.css" />
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

</head>
<body>
  <div class="pokedex">
    <div class="camera"></div>
    <div class="light red"></div>
    <div class="light yellow"></div>
    <div class="light green"></div>

    <!-- PANTALLA (blanca) -->
    <div class="screen">
      <div class="screen-content">
        <img class="sprite" alt="sprite" />
        <h2 class="poke-name"></h2>
        <div class="loading">Cargando…</div>
        <div class="error"></div>
      </div>
      <!-- (QUITADO) botón rojo inferior izquierdo -->
      <!-- <div class="button-red"></div> -->
    </div>

    <!-- Botones decorativos -->
    <div class="small-buttons">
      <div class="btn red"></div>
      <div class="btn green"></div>
    </div>

    <!-- Buscador (izquierda de flechas) -->
    <form class="search-wrap" id="searchForm" autocomplete="off">
      <input id="searchInput" type="text" placeholder="Nombre o #ID" />
      <button id="searchBtn" type="submit" aria-label="Buscar">🔎</button>
    </form>

    <!-- Flechas -->
    <div class="nav-buttons">
      <button class="nav-btn left" id="prevBtn" aria-label="Anterior"></button>
      <button class="nav-btn right" id="nextBtn" aria-label="Siguiente"></button>
    </div>

    <!-- Display inferior (verde grande) -->
    <div class="bottom-display">
      <ul class="stats">
        <li><span>ID</span> <b id="stat-id"></b></li>
        <li><span>Tipos</span> <b id="stat-types"></b></li>
        <li><span>Altura</span> <b id="stat-height"></b></li>
        <li><span>Peso</span> <b id="stat-weight"></b></li>
        <li class="col-2"><span>Habilidades</span> <b id="stat-abilities"></b></li>
      </ul>
    </div>
  </div>

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

Por último app.js

“Mesero/menú/JSON”fetch(API + q) es justo “pedir el platillo”.

UI retro en CSS.sprite, .poke-name, stats… son los “platos” que pintamos cuando llega el JSON.

Localización al español → pides otros endpoints (species, type, ability) y filtras language.name === "es".

UX linda → el blink de la cámara y el loading son feedback visual mientras cocina la orden.

Accesos múltiples → flechas, buscador, y teclas ← → / Enter.

JavaScript
// =====================================================
// 1) Config y estado
// =====================================================
const API = "https://pokeapi.co/api/v2/pokemon/";
let currentId = 1;

// =====================================================
// 2) Referencias a la UI (DOM)
// =====================================================
const sprite      = document.querySelector(".sprite");
const nameEl      = document.querySelector(".poke-name");
const loadingEl   = document.querySelector(".loading");
const errorEl     = document.querySelector(".error");
const cameraEl    = document.querySelector(".camera");

const idEl        = document.getElementById("stat-id");
const typesEl     = document.getElementById("stat-types");
const heightEl    = document.getElementById("stat-height");
const weightEl    = document.getElementById("stat-weight");
const abilitiesEl = document.getElementById("stat-abilities");

const prevBtn     = document.getElementById("prevBtn");
const nextBtn     = document.getElementById("nextBtn");
const searchForm  = document.getElementById("searchForm");
const searchInput = document.getElementById("searchInput");

// =====================================================
// 3) Utilidades de UI
// =====================================================
const cap = s => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;

const showLoading = on => {
  loadingEl.style.display = on ? "block" : "none";
};

const showError = msg => {
  if (!msg) {
    errorEl.style.display = "none";
    errorEl.textContent = "";
    return;
  }
  errorEl.textContent = msg;
  errorEl.style.display = "block";
};

// =====================================================
// 4) Helpers para ES (con caché por URL)
//    - species: nombres/description en español
//    - type / ability: nombres localizados
// =====================================================
const _cache = new Map();

async function getJSON(url){
  if (_cache.has(url)) return _cache.get(url);
  const r = await fetch(url);
  if (!r.ok) throw new Error("Error: " + url);
  const j = await r.json();
  _cache.set(url, j);
  return j;
}

const findEs = arr => (arr || []).find(x => x.language?.name === "es")?.name;

async function getSpeciesEs(idOrName){
  const s = await getJSON(`https://pokeapi.co/api/v2/pokemon-species/${idOrName}`);
  return {
    name:   findEs(s.names) || s.name,
    genus:  (s.genera || []).find(g => g.language?.name === "es")?.genus || "",
    flavor: (s.flavor_text_entries || [])
              .find(f => f.language?.name === "es")
              ?.flavor_text?.replace(/\f/g," ") || ""
  };
}

async function getTypeNameEs(url){
  const t = await getJSON(url);
  return findEs(t.names) || t.name;
}

async function getAbilityNameEs(url){
  const a = await getJSON(url);
  return findEs(a.names) || a.name;
}

// =====================================================
// 5) Cargar y pintar Pokémon
//    - Blink de cámara mientras carga
//    - Fetch principal (pokemon)
//    - Traducciones ES (species, types, abilities)
//    - Pintar UI / manejar errores
// =====================================================
async function loadPokemon(target) {
  cameraEl.classList.add("blink");

  try {
    showError("");
    showLoading(true);

    // 5.1 Normalizar query (id o nombre)
    const q = String(target).toLowerCase().trim();

    // 5.2 Datos base del Pokémon
    const res = await fetch(API + q);
    if (!res.ok) throw new Error("Pokémon no encontrado");
    const data = await res.json();
    currentId = data.id;

    // 5.3 Sprite: preferimos official-artwork > home > front_default
    const art = data.sprites?.other?.["official-artwork"]?.front_default
             || data.sprites?.other?.home?.front_default
             || data.sprites?.front_default;

    sprite.src = art ?? "";
    sprite.alt = data.name; // (podrías poner speciesEs.name más abajo si prefieres)

    // 5.4 Traducciones en paralelo (rápido y limpio)
    const [speciesEs, typesEs, abilitiesEs] = await Promise.all([
      getSpeciesEs(data.id),
      Promise.all(data.types.map(t => getTypeNameEs(t.type.url))),
      Promise.all(data.abilities.map(a => getAbilityNameEs(a.ability.url)))
    ]);

    // 5.5 Pintar UI
    nameEl.textContent   = `#${String(data.id).padStart(3,"0")} · ${cap(speciesEs.name)}`;
    idEl.textContent     = `#${data.id}`;
    typesEl.textContent  = typesEs.join(" / ") || "—";
    heightEl.textContent = `${(data.height/10).toFixed(1)} m`;
    weightEl.textContent = `${(data.weight/10).toFixed(1)} kg`;
    abilitiesEl.textContent = abilitiesEs.slice(0,3).join(", ");

  } catch (err) {
    // 5.6 Fallback en errores (no rompe la UI)
    showError(err.message || "Error");
    nameEl.textContent = "—";
    sprite.removeAttribute("src");
    idEl.textContent = typesEl.textContent = heightEl.textContent =
      weightEl.textContent = abilitiesEl.textContent = "—";
  } finally {
    // 5.7 Cerrar loading y parar blink siempre
    showLoading(false);
    cameraEl.classList.remove("blink");
  }
}

// =====================================================
// 6) Eventos (clicks, submit, teclado)
// =====================================================
prevBtn.addEventListener("click", () => {
  if (currentId > 1) loadPokemon(currentId - 1);
});

nextBtn.addEventListener("click", () => {
  loadPokemon(currentId + 1);
});

searchForm.addEventListener("submit", (e) => {
  e.preventDefault();
  const q = searchInput.value.trim();
  if (q) loadPokemon(q);
});

// Teclas: ← → navegan; Enter busca si estás en el input
window.addEventListener("keydown", (e) => {
  if (e.key === "ArrowLeft")  { e.preventDefault(); if (currentId > 1) loadPokemon(currentId - 1); }
  if (e.key === "ArrowRight") { e.preventDefault(); loadPokemon(currentId + 1); }
  if (e.key === "Enter" && document.activeElement === searchInput) {
    e.preventDefault();
    const q = searchInput.value.trim();
    if (q) loadPokemon(q);
  }
});

// =====================================================
// 7) Carga inicial
// =====================================================
loadPokemon(1);

Resultado FINAL

Ver pokedex

Ver pokedex