Tienes una página web que funciona: HTML, algo de CSS, JavaScript que manipula el DOM. Todo estático. Lo siguiente es hacer que traiga datos reales de internet — el clima de ahora mismo, un perfil de GitHub, resultados de una búsqueda.
Para eso existe fetch(). Una función de JavaScript que hace una petición a una URL y devuelve lo que haya ahí. Este tutorial va de usarla bien desde el primer día: con manejo de errores, sin vulnerabilidades y con datos reales.
¿Qué es una API?
Detrás de casi cualquier web hay un servidor con datos. Ese servidor tiene URLs específicas para pedirlos: una para el perfil de un usuario, otra para el tiempo, otra para los resultados de búsqueda. Cuando tu página web llama a una de esas URLs y recibe datos en formato JSON, eso es usar una API.
Lo que cambia respecto a cargar una página normal es que no recibes HTML que el navegador pinta — recibes datos en crudo que tú decides cómo mostrar. Eso es lo que te permite construir interfaces dinámicas sin recargar la página.
fetch(): cómo funciona
fetch() recibe una URL y devuelve una Promesa — JavaScript no bloquea el resto del código mientras espera la respuesta del servidor. La estructura básica:
fetch('https://api-example.com/datos')
.then(response => response.json())
.then(data => {
// Aquí ya tienes los datos para usar
console.log(data);
});
fetch(url) lanza la petición. El primer .then() convierte la respuesta a objeto JavaScript con response.json(). El segundo .then() ya tiene los datos listos.
El error más frecuente aquí, y donde la mayoría se atasca la primera vez: intentar usar response directamente como si ya fueran los datos. No lo son — response es el objeto de la respuesta HTTP. Hay que llamar a response.json() primero. Y como eso también devuelve una Promesa, necesitas dos .then(), no uno.
Ejemplos con datos reales
Tres ejemplos con APIs públicas. Cada uno introduce algo nuevo.
Nivel 1: Tu primera llamada API
Empezamos con algo simple: traer una cita aleatoria desde una API pública, sin registro ni clave.
Haz clic para obtener una cita inspiracional
Una cosa que no se ve en el demo pero que es importante: el código usa createElement y textContent en lugar de innerHTML. Aquí está el porqué.
Si usas innerHTML con datos de una API, cualquier <script> que venga en la respuesta se ejecuta en tu página. Es una vulnerabilidad XSS clásica. Este código lo demuestra para que sepas qué evitar:
display.innerHTML = `<p>"${data.text}"</p>`; // ⚠️ XSS vulnerable!
Si la API devuelve algo como <script>alert('hack')</script>, se ejecutará en tu página — aunque eso no fuera tu intención.
La versión segura usa createElement y textContent. textContent trata todo como texto plano, nunca ejecuta HTML:
// 1. Encontrar elementos
const boton = document.querySelector('#quote-btn');
const display = document.querySelector('#quote-display');
// 2. Función para obtener cita
async function obtenerCita() {
try {
const response = await fetch('https://thequoteshub.com/api/');
const data = await response.json();
// 3. Crear elementos DOM de forma SEGURA
display.replaceChildren(); // Limpiar
const quoteText = document.createElement('p');
quoteText.className = 'quote-text';
quoteText.textContent = `"${data.text}"`;
const quoteAuthor = document.createElement('p');
quoteAuthor.className = 'quote-author';
quoteAuthor.textContent = `- ${data.author}`;
// 4. Agregar al DOM
display.appendChild(quoteText);
display.appendChild(quoteAuthor);
} catch (error) {
display.replaceChildren();
const errorMsg = document.createElement('p');
errorMsg.textContent = 'Error: No se pudo obtener la cita';
display.appendChild(errorMsg);
}
}
// 5. Escuchar el click
boton.addEventListener('click', obtenerCita);
Aunque la API devuelva <script>alert('hack')</script>, textContent lo muestra como texto inofensivo en lugar de ejecutarlo.
Otro punto que confunde bastante al principio: cada API devuelve un JSON con claves propias. Si usas la clave equivocada, obtienes undefined sin ningún error visible, y no sabes por qué tu pantalla está en blanco.
{
"text": "La vida es lo que pasa...",
"author": "John Lennon",
"id": 12345
}
data.text // ✅ Correcto
data.author // ✅ Correcto
data.content // ❌ undefined!
data.quote // ❌ undefined!
Antes de escribir código, haz un console.log(data) y mira la estructura real que devuelve la API. Es la forma más rápida de saber qué claves usar.
Acceder a datos JSON anidados
Las respuestas de las APIs no suelen ser planas. Son estructuras con arrays dentro de objetos, objetos dentro de arrays, varios niveles de anidación. Aquí es donde más gente se pierde.
Anatomía de una respuesta JSON real
Ejemplo de respuesta típica de una API de clima:
{
"coord": {
"lon": -3.7038,
"lat": 40.4168
},
"weather": [
{
"id": 800,
"main": "Clear",
"description": "cielo claro",
"icon": "01d"
}
],
"main": {
"temp": 22.5,
"feels_like": 21.8,
"humidity": 45
},
"sys": {
"country": "ES",
"sunrise": 1692772892
},
"name": "Madrid"
}
Cómo acceder a cada propiedad
Propiedades simples (nivel 1)
data.name // "Madrid"
Regla: Usa el punto (.) para acceder a propiedades directas
Objetos anidados (nivel 2)
data.coord.lon // -3.7038
data.coord.lat // 40.4168
data.main.temp // 22.5
data.sys.country // "ES"
Regla: Encadena puntos para "navegar" dentro de objetos
Arrays con objetos (nivel 2+)
data.weather // El array completo
data.weather[0] // El primer elemento del array
data.weather[0].main // "Clear"
data.weather[0].description // "cielo claro"
Regla: Usa [0] para acceder al primer elemento, [1] al segundo, etc.
Errores frecuentes accediendo a JSON
Error #1: Array vacío
data.weather[0].main // Error si weather está vacío!
// Verificar si existe y tiene elementos
if (data.weather && data.weather.length > 0) {
const weatherMain = data.weather[0].main;
}
// O usando operador opcional (?)
const weatherMain = data.weather?.[0]?.main || 'No disponible';
Error #2: Propiedad inexistente
data.main.pressure // undefined si no existe
// Verificar antes de usar
const pressure = data.main.pressure || 'No disponible';
// O con operador opcional
const pressure = data.main?.pressure ?? 'No disponible';
Práctica con la API de GitHub
La API de GitHub devuelve datos bastante anidados. Buen campo de pruebas:
{
"login": "octocat",
"id": 1,
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"name": "The Octocat",
"company": "@github",
"public_repos": 8,
"followers": 4008,
"following": 9,
"created_at": "2011-01-25T18:44:36Z",
"plan": {
"name": "pro",
"space": 976562499,
"private_repos": 9999
}
}
Cómo extraer cada dato:
// ✅ Datos simples
const username = data.login; // "octocat"
const realName = data.name; // "The Octocat"
const followers = data.followers; // 4008
// ✅ Objeto anidado
const planName = data.plan.name; // "pro"
const planSpace = data.plan.space; // 976562499
// ✅ Acceso SEGURO (por si plan no existe)
const safePlanName = data.plan?.name || 'No tiene plan';
API con arrays de objetos anidados
Ejemplo de una API de posts con comentarios — la estructura más frecuente en backends reales:
{
"posts": [
{
"id": 1,
"title": "Mi primer post",
"author": {
"name": "Juan Pérez",
"avatar": "https://example.com/juan.jpg",
"social": {
"twitter": "@juanperez",
"github": "juanperez"
}
},
"comments": [
{
"id": 101,
"text": "¡Excelente post!",
"author": {
"name": "María García"
}
},
{
"id": 102,
"text": "Muy útil, gracias",
"author": {
"name": "Carlos López"
}
}
]
}
]
}
Acceso paso a paso:
// 1. ✅ Obtener el primer post
const firstPost = data.posts[0];
// 2. ✅ Datos del post
const postTitle = firstPost.title; // "Mi primer post"
// 3. ✅ Autor del post (objeto anidado)
const authorName = firstPost.author.name; // "Juan Pérez"
// 4. ✅ Social del autor (objeto anidado dentro de otro objeto)
const authorTwitter = firstPost.author.social.twitter; // "@juanperez"
// 5. ✅ Primer comentario
const firstComment = firstPost.comments[0];
const commentText = firstComment.text; // "¡Excelente post!"
// 6. ✅ Autor del comentario
const commentAuthor = firstComment.author.name; // "María García"
Recorrer todos los comentarios con un bucle:
// ✅ Mostrar todos los comentarios del primer post
firstPost.comments.forEach((comment, index) => {
console.log(`Comentario ${index + 1}:`);
console.log(`- ${comment.text}`);
console.log(`- Por: ${comment.author.name}`);
});
// ✅ Versión SEGURA (por si no hay comentarios)
if (firstPost.comments && firstPost.comments.length > 0) {
firstPost.comments.forEach(comment => {
// Procesar comentarios de forma segura
const authorName = comment.author?.name || 'Anónimo';
console.log(`${comment.text} - ${authorName}`);
});
} else {
console.log('No hay comentarios en este post');
}
Tres cosas que te ahorran tiempo cuando los datos no salen bien: haz siempre un console.log(data) antes de acceder a nada — así ves la estructura real. Si necesitas ver las claves disponibles, Object.keys(data). Y en la pestaña Network de Chrome DevTools (F12 → Network → XHR) puedes ver la respuesta completa de cada llamada y navegar el JSON con la pestaña Preview.
Nivel 2: Buscador de usuarios de GitHub
Buscar perfiles reales de GitHub — la API es pública y no necesita clave.
Busca cualquier usuario de GitHub (ej: octocat, torvalds, gaearon)
HTML necesario:
<input type="text" id="github-input" placeholder="Usuario de GitHub...">
<button id="github-btn">Buscar</button>
<div id="github-result"></div>
JavaScript del buscador de GitHub:
const input = document.querySelector('#github-input');
const boton = document.querySelector('#github-btn');
const resultado = document.querySelector('#github-result');
async function buscarUsuario() {
const username = input.value.trim();
// Validación SEGURA
if (username === '') {
resultado.replaceChildren();
const errorMsg = document.createElement('p');
errorMsg.textContent = 'Por favor escribe un nombre de usuario';
resultado.appendChild(errorMsg);
return;
}
// Loading SEGURO
resultado.replaceChildren();
const loading = document.createElement('p');
loading.textContent = 'Buscando...';
resultado.appendChild(loading);
try {
const response = await fetch(`https://api.github.com/users/${username}`);
if (!response.ok) {
throw new Error('Usuario no encontrado');
}
const data = await response.json();
// Crear tarjeta SEGURA
resultado.replaceChildren();
const userCard = document.createElement('div');
userCard.className = 'user-card';
const avatar = document.createElement('img');
avatar.src = data.avatar_url; // Seguro: propiedad directa
avatar.alt = 'Avatar';
avatar.className = 'avatar';
const name = document.createElement('h3');
name.textContent = data.name || data.login; // Seguro: solo texto
userCard.appendChild(avatar);
userCard.appendChild(name);
// ... más elementos
resultado.appendChild(userCard);
} catch (error) {
resultado.replaceChildren();
const errorMsg = document.createElement('p');
errorMsg.textContent = `❌ ${error.message}`;
resultado.appendChild(errorMsg);
}
}
boton.addEventListener('click', buscarUsuario);
Nivel 3: App del clima
El ejemplo clásico para practicar fetch: mostrar el clima de cualquier ciudad. Esta versión necesita una API key de OpenWeatherMap (gratuita con registro).
Prueba con: Madrid, Barcelona, Buenos Aires, Ciudad de México
JavaScript del clima:
// API key gratuita (en producción, mantén esto secreto)
const API_KEY = 'TU_API_KEY_AQUI';
const BASE_URL = 'https://api.openweathermap.org/data/2.5/weather';
async function obtenerClima() {
const ciudad = document.querySelector('#city-input').value.trim();
const resultado = document.querySelector('#weather-result');
if (ciudad === '') return;
// Loading SEGURO
resultado.replaceChildren();
const loading = document.createElement('p');
loading.textContent = 'Obteniendo clima...';
resultado.appendChild(loading);
try {
const url = `${BASE_URL}?q=${ciudad}&appid=${API_KEY}&units=metric&lang=es`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('Ciudad no encontrada');
}
const data = await response.json();
// Crear weather card de forma SEGURA
resultado.replaceChildren();
const weatherCard = document.createElement('div');
weatherCard.className = 'weather-card';
const title = document.createElement('h3');
title.textContent = `🌍 ${data.name}, ${data.sys.country}`;
const temperature = document.createElement('div');
temperature.className = 'temperature';
temperature.textContent = `${Math.round(data.main.temp)}°C`;
const description = document.createElement('p');
description.className = 'description';
description.textContent = data.weather[0].description;
// Ensamblar elementos
weatherCard.appendChild(title);
weatherCard.appendChild(temperature);
weatherCard.appendChild(description);
resultado.appendChild(weatherCard);
} catch (error) {
resultado.replaceChildren();
const errorMsg = document.createElement('p');
errorMsg.textContent = `❌ ${error.message}`;
resultado.appendChild(errorMsg);
}
}
Errores comunes con fetch
Error #1: No manejar errores
fetch(url).then(data => {
// ¿Y si falla?
});
fetch(url)
.then(data => {/* éxito */})
.catch(error => {/* error */});
Por qué: Las APIs pueden fallar. Internet puede fallar. Siempre maneja errores.
Error #2: No verificar response.ok
fetch(url)
.then(response => response.json()) // ¡Error 404!
fetch(url)
.then(response => {
if (!response.ok) throw new Error('Error HTTP');
return response.json();
})
Por qué: fetch() no rechaza automáticamente códigos 404, 500, etc.
Error #3: API keys expuestas
// En el frontend, visible para todos
const apiKey = 'sk-1234567890abcdef';
// Usar APIs públicas sin key, o
// Hacer llamadas desde tu backend
Por qué: Cualquiera puede ver tu API key y usarla (y cobrarte).
Error #4: No mostrar estados de carga
// Usuario hace click... ¿pasa algo?
fetch(url).then(...);
button.textContent = 'Cargando...';
fetch(url).then(...);
Por qué: Las APIs pueden tardar. El usuario necesita feedback visual.
APIs públicas para practicar
Estas no requieren registro y puedes usarlas ahora mismo:
🎭 Random Quotes
https://thequoteshub.com/api/
Citas célebres aleatorias. La que usamos en este tutorial.
🐕 Dog Photos
https://dog.ceo/api/breeds/image/random
Fotos random de perritos. ✅ Funciona perfectamente.
📊 Datos de Prueba
https://httpbin.org/uuid
Genera UUID únicos. Perfecta para IDs aleatorios.
😂 Chuck Norris Facts
https://api.chucknorris.io/jokes/random
Chistes de Chuck Norris. ✅ Funciona y garantiza risas.
🏛️ GitHub API
https://api.github.com/users/octocat
Datos públicos de usuarios. ✅ Perfecta para portfolios.
🌐 APIs Públicas
https://httpbin.org/json
Datos JSON de prueba. Siempre disponible para testing.
Proyecto completo: mini dashboard
Una mini-aplicación que combina varias APIs a la vez:
Haz clic en cualquier botón para traer contenido desde diferentes APIs
Para continuar con esto: añade una API diferente al dashboard, guarda los resultados en localStorage para persistirlos entre sesiones, o combina dos APIs en la misma tarjeta (imagen de un perrito con un dato curioso al lado).
async/await: la alternativa más legible
async/await no es algo diferente a las Promesas — es azúcar sintáctico que las hace parecer código síncrono. El resultado es idéntico, pero el código es más fácil de leer y de depurar:
📜 Forma tradicional (.then)
fetch(url)
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
✨ Forma moderna (async/await)
async function obtenerDatos() {
try {
const response = await fetch(url);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
Para empezar usa .then() — entiendes mejor lo que pasa con las Promesas. Cuando te sientas cómodo, cambia a async/await: el código queda más limpio y el try/catch es más claro que el .catch() encadenado. Ambas hacen exactamente lo mismo.
Tienes la explicación completa de ese cambio, con más ejemplos y los errores típicos al migrar de uno a otro, en Async/Await, adiós callback hell.
Qué toca ahora
El siguiente paso concreto es tomar el buscador de GitHub de este tutorial, darle estilos CSS propios y publicarlo en GitHub Pages. En media hora tienes algo con datos reales que puedes poner en un portfolio.
Cuando eso funcione, el salto natural es aprender a crear tu propia API con Python y Flask. En ese momento dejas de consumir datos de otros y empiezas a servir los tuyos. Si en cambio prefieres tirar hacia frontend, fetch es una de las bases que necesitas antes de meterte en React, así que te puede interesar el roadmap completo de React.
Otro proyecto que te obliga a combinar fetch con datos que persisten es un carrito de compra con JavaScript y localStorage: pides los productos a una API y guardas lo que el usuario añade al carrito para que no se pierda al recargar.
Una vez que fetch básico ya no se te resiste, el siguiente nivel es tipar tus llamadas a la API con TypeScript, para que el editor te avise si accedes a una propiedad que la respuesta no tiene.
Tres cosas que marcan la diferencia
Prueba la API en Postman antes de escribir el código. Así ves exactamente qué devuelve y qué claves usar, sin tener que adivinar. Te ahorra más tiempo del que parece.
Muestra siempre un estado de carga. Si el usuario hace clic y no pasa nada visible durante dos segundos, asume que está roto. Un simple texto "Cargando..." o deshabilitar el botón mientras fetch trabaja lo soluciona.
Maneja el error de red. fetch() solo rechaza la Promesa si hay un fallo de red — no si el servidor responde con 404 o 500. Comprueba siempre response.ok antes de intentar parsear el JSON, o tendrás errores crípticos de JSON inválido cuando lo que falló fue la petición.