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.
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
<!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.
// =====================================================
// 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