APIs para principiantes: Tu primera llamada fetch() con JavaScript

Conecta tu página web con el mundo exterior: trae datos reales de internet y crea aplicaciones que realmente funcionan

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:

Sintaxis básica de fetch() 📋
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

Cargando...

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:

❌ INSEGURO (no copies esto):
Código inseguro - NO usar
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:

Implementación completa y segura de fetch
// 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.

❌ Esta API devuelve:
Estructura de respuesta JSON de la API
{
  "text": "La vida es lo que pasa...",
  "author": "John Lennon",
  "id": 12345
}
✅ Por eso usamos:
Acceso correcto e incorrecto a propiedades
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:

Ejemplo de respuesta JSON compleja con objetos y arrays anidados
{
  "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

1
Propiedades simples (nivel 1)
Acceso a propiedades simples
data.name        // "Madrid"

Regla: Usa el punto (.) para acceder a propiedades directas

2
Objetos anidados (nivel 2)
Acceso a objetos anidados
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

3
Arrays con objetos (nivel 2+)
Acceso a arrays con objetos
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

❌ Problema:
Código problemático - Acceso sin verificar
data.weather[0].main // Error si weather está vacío!
✅ Solución SEGURA:
Verificación segura de arrays
// 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

❌ Problema:
Código problemático - Propiedad no verificada
data.main.pressure // undefined si no existe
✅ Solución SEGURA:
Verificación segura de propiedades
// 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:

Extracción de datos de la respuesta de GitHub
// ✅ 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:

Estructura compleja: API con arrays de objetos anidados
{
  "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:

Acceso paso a paso a estructura compleja
// 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:

Recorrer arrays de forma segura
// ✅ 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)

Buscando usuario...

HTML necesario:

HTML para el buscador de GitHub
<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:

Obtener datos del usuario 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:

App del clima completa y segura
// 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

❌ Malo:
Código problemático sin manejo de errores
fetch(url).then(data => {
    // ¿Y si falla?
});
✅ Bueno:
Manejo correcto de errores
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

❌ Problema:
Error: No verificar response.ok
fetch(url)
  .then(response => response.json()) // ¡Error 404!
✅ Solución:
Verificación correcta de response.ok
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

❌ Peligroso:
PELIGRO: API key expuesta en frontend
// En el frontend, visible para todos
const apiKey = 'sk-1234567890abcdef';
✅ Seguro:
Alternativas seguras para API keys
// 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

❌ Malo:
Sin feedback visual de carga
// Usuario hace click... ¿pasa algo?
fetch(url).then(...);
✅ Bueno:
Con feedback visual de carga
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)

Método tradicional con .then()
fetch(url)
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error(error);
  });

✨ Forma moderna (async/await)

Método moderno con 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.