Volver al curso

JavaScript Desde Cero: Tu Primer Lenguaje de Programación

leccion
18 / 22
beginner
8 horas
JavaScript Asíncrono

Fetch API: Consumiendo APIs del Mundo Real

Lectura
45 min~8 min lectura

Fetch API: Consumiendo APIs del Mundo Real

Objetivos de aprendizaje

Al finalizar esta lección serás capaz de:

  • Usar fetch() para hacer peticiones HTTP (GET, POST, PUT, DELETE)
  • Manejar respuestas JSON, texto y errores HTTP correctamente
  • Consumir APIs públicas reales para obtener datos dinámicos
  • Implementar un CRUD completo usando fetch con async/await
  • Construir interfaces que muestren datos de APIs en tiempo real

1. Fundamentos de Fetch API

fetch() es la función nativa de JavaScript para hacer peticiones HTTP. Reemplazó a XMLHttpRequest (AJAX) y es la base de toda comunicación con servidores.

GET: obtener datos

// Petición GET básica
async function obtenerUsuarios() {
  try {
    const respuesta = await fetch("https://jsonplaceholder.typicode.com/users");
    
    // Verificar que la respuesta sea exitosa
    if (!respuesta.ok) {
      throw new Error(`HTTP ${respuesta.status}: ${respuesta.statusText}`);
    }
    
    const usuarios = await respuesta.json(); // Parsear JSON
    console.log(`${usuarios.length} usuarios obtenidos`);
    return usuarios;
  } catch (error) {
    console.error("Error al obtener usuarios:", error.message);
    return [];
  }
}

// Propiedades del objeto Response
async function analizarRespuesta() {
  const respuesta = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  
  console.log(respuesta.ok);         // true (status 200-299)
  console.log(respuesta.status);     // 200
  console.log(respuesta.statusText); // "OK"
  console.log(respuesta.headers.get("Content-Type")); // "application/json"
  console.log(respuesta.url);        // URL final (después de redirects)
  
  // Métodos para leer el body (solo se puede leer UNA vez)
  const datos = await respuesta.json();  // Parsear como JSON
  // const texto = await respuesta.text();  // Leer como texto
  // const blob = await respuesta.blob();   // Leer como binario
}

POST: enviar datos

async function crearPost(titulo, cuerpo, userId) {
  try {
    const respuesta = await fetch("https://jsonplaceholder.typicode.com/posts", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        title: titulo,
        body: cuerpo,
        userId: userId
      })
    });
    
    if (!respuesta.ok) {
      throw new Error(`Error al crear post: ${respuesta.status}`);
    }
    
    const nuevoPost = await respuesta.json();
    console.log("Post creado:", nuevoPost);
    return nuevoPost;
  } catch (error) {
    console.error(error.message);
    return null;
  }
}

// Llamar
const post = await crearPost("Mi primer post", "Contenido del post...", 1);

PUT/PATCH: actualizar datos

// PUT: reemplazar el recurso completo
async function actualizarPost(id, datos) {
  const respuesta = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(datos)
  });
  return respuesta.json();
}

// PATCH: actualizar parcialmente
async function actualizarTitulo(id, nuevoTitulo) {
  const respuesta = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title: nuevoTitulo })
  });
  return respuesta.json();
}

DELETE: eliminar datos

async function eliminarPost(id) {
  const respuesta = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
    method: "DELETE"
  });
  
  if (respuesta.ok) {
    console.log(`Post ${id} eliminado`);
    return true;
  }
  return false;
}

2. Consumiendo APIs públicas reales

API de clima (OpenWeatherMap style)

async function obtenerClima(ciudad) {
  try {
    // Usando una API pública gratuita
    const respuesta = await fetch(
      `https://api.openweathermap.org/data/2.5/weather?q=${ciudad}&units=metric&lang=es&appid=TU_API_KEY`
    );
    
    if (!respuesta.ok) {
      if (respuesta.status === 404) throw new Error("Ciudad no encontrada");
      throw new Error(`Error: ${respuesta.status}`);
    }
    
    const datos = await respuesta.json();
    
    return {
      ciudad: datos.name,
      temperatura: Math.round(datos.main.temp),
      sensacion: Math.round(datos.main.feels_like),
      humedad: datos.main.humidity,
      descripcion: datos.weather[0].description,
      icono: `https://openweathermap.org/img/wn/${datos.weather[0].icon}@2x.png`,
      viento: datos.wind.speed
    };
  } catch (error) {
    console.error("Error al obtener clima:", error.message);
    return null;
  }
}

// Uso
const clima = await obtenerClima("Buenos Aires");
if (clima) {
  console.log(`${clima.ciudad}: ${clima.temperatura}°C, ${clima.descripcion}`);
}

API de países (REST Countries)

async function buscarPais(nombre) {
  try {
    const respuesta = await fetch(`https://restcountries.com/v3.1/name/${nombre}`);
    if (!respuesta.ok) throw new Error("País no encontrado");
    
    const paises = await respuesta.json();
    
    return paises.map(pais => ({
      nombre: pais.name.common,
      nombreOficial: pais.name.official,
      capital: pais.capital?.[0] || "N/A",
      poblacion: pais.population.toLocaleString(),
      region: pais.region,
      bandera: pais.flags.svg,
      moneda: Object.values(pais.currencies || {})[0]?.name || "N/A",
      idiomas: Object.values(pais.languages || {}).join(", ")
    }));
  } catch (error) {
    console.error(error.message);
    return [];
  }
}

const paises = await buscarPais("argentina");
console.table(paises);

API de GitHub

async function perfilGitHub(username) {
  const [usuario, repos] = await Promise.all([
    fetch(`https://api.github.com/users/${username}`).then(r => r.json()),
    fetch(`https://api.github.com/users/${username}/repos?sort=stars&per_page=5`).then(r => r.json())
  ]);
  
  return {
    nombre: usuario.name,
    bio: usuario.bio,
    avatar: usuario.avatar_url,
    seguidores: usuario.followers,
    reposPublicos: usuario.public_repos,
    topRepos: repos.map(r => ({
      nombre: r.name,
      estrellas: r.stargazers_count,
      lenguaje: r.language,
      url: r.html_url
    }))
  };
}

const perfil = await perfilGitHub("octocat");
console.log(perfil);

3. Patrones profesionales con Fetch

Wrapper de API reutilizable

class ApiClient {
  constructor(baseURL, opciones = {}) {
    this.baseURL = baseURL;
    this.headers = {
      "Content-Type": "application/json",
      ...opciones.headers
    };
  }
  
  async request(endpoint, opciones = {}) {
    const url = `${this.baseURL}${endpoint}`;
    
    try {
      const respuesta = await fetch(url, {
        headers: this.headers,
        ...opciones
      });
      
      if (!respuesta.ok) {
        const error = await respuesta.json().catch(() => ({}));
        throw new Error(error.message || `HTTP ${respuesta.status}`);
      }
      
      return await respuesta.json();
    } catch (error) {
      console.error(`[API Error] ${opciones.method || 'GET'} ${url}:`, error.message);
      throw error;
    }
  }
  
  get(endpoint) { return this.request(endpoint); }
  
  post(endpoint, data) {
    return this.request(endpoint, {
      method: "POST",
      body: JSON.stringify(data)
    });
  }
  
  put(endpoint, data) {
    return this.request(endpoint, {
      method: "PUT",
      body: JSON.stringify(data)
    });
  }
  
  delete(endpoint) {
    return this.request(endpoint, { method: "DELETE" });
  }
}

// Uso
const api = new ApiClient("https://jsonplaceholder.typicode.com");

const posts = await api.get("/posts");
const nuevoPost = await api.post("/posts", { title: "Hola", body: "Mundo" });
await api.delete("/posts/1");

Manejo de loading states

async function cargarConEstado(btn, contenedor, fetchFn) {
  const textoOriginal = btn.textContent;
  
  try {
    // Estado: cargando
    btn.disabled = true;
    btn.textContent = "Cargando...";
    contenedor.innerHTML = '<div class="skeleton">Cargando datos...</div>';
    
    // Obtener datos
    const datos = await fetchFn();
    
    // Estado: éxito
    contenedor.innerHTML = renderizarDatos(datos);
  } catch (error) {
    // Estado: error
    contenedor.innerHTML = `
      <div class="error">
        <p>Error al cargar los datos: ${error.message}</p>
        <button onclick="location.reload()">Reintentar</button>
      </div>
    `;
  } finally {
    // Restaurar botón
    btn.disabled = false;
    btn.textContent = textoOriginal;
  }
}

// Uso
const btn = document.querySelector("#cargar-btn");
const contenedor = document.querySelector("#datos");

btn.addEventListener("click", () => {
  cargarConEstado(btn, contenedor, () => 
    fetch("https://api.ejemplo.com/datos").then(r => r.json())
  );
});

Search con debounce

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const searchInput = document.querySelector("#busqueda");
const resultados = document.querySelector("#resultados");

const buscarPaises = debounce(async (termino) => {
  if (termino.length < 2) {
    resultados.innerHTML = "";
    return;
  }
  
  try {
    resultados.innerHTML = "<p>Buscando...</p>";
    const respuesta = await fetch(`https://restcountries.com/v3.1/name/${termino}`);
    
    if (!respuesta.ok) {
      resultados.innerHTML = "<p>No se encontraron resultados</p>";
      return;
    }
    
    const paises = await respuesta.json();
    resultados.innerHTML = paises.slice(0, 5).map(p => `
      <div class="resultado">
        <img src="${p.flags.svg}" width="30" alt="${p.name.common}">
        <span>${p.name.common}</span>
        <small>${p.capital?.[0] || ''}</small>
      </div>
    `).join("");
  } catch (error) {
    resultados.innerHTML = `<p class="error">Error: ${error.message}</p>`;
  }
}, 300);

searchInput.addEventListener("input", (e) => {
  buscarPaises(e.target.value.trim());
});

Errores comunes de principiantes
  1. No verificar response.ok: fetch NO rechaza en errores HTTP (404, 500). Solo rechaza en errores de red. Siempre verificá response.ok.

  2. Leer el body dos veces: response.json() y response.text() solo se pueden llamar UNA vez. Guardá el resultado en una variable.

  3. Olvidar JSON.stringify() en el body del POST: El body debe ser un string, no un objeto.

  4. No manejar CORS: Si la API no permite tu dominio, vas a ver errores de CORS. Esto es una restricción del servidor, no un bug tuyo.

  5. No usar AbortController para cancelar peticiones: Si el usuario navega a otra página mientras se carga, la petición sigue activa. Usá AbortController para cancelarla.


Puntos clave de esta lección
  1. fetch() retorna una Promise con el objeto Response — siempre verificá response.ok.
  2. Usá response.json() para parsear JSON, response.text() para texto plano.
  3. Para POST/PUT, necesitás: method, headers con Content-Type, y body con JSON.stringify().
  4. Promise.all() con fetch permite cargar datos de múltiples APIs en paralelo.
  5. Siempre manejá los tres estados: loading, success y error.
  6. Un ApiClient wrapper reutilizable simplifica mucho el trabajo con APIs.
  7. Usá debounce para búsquedas en tiempo real y evitar exceso de peticiones.

Quiz de autoevaluación

1. ¿Qué retorna fetch()?
a) Los datos directamente
b) Una Promise que se resuelve con un objeto Response
c) Un callback
d) Un string JSON

2. ¿fetch rechaza la Promise en un error HTTP 404?
a) Sí, siempre
b) No, solo rechaza en errores de red. Hay que verificar response.ok
c) Solo si se configura
d) Depende del navegador

3. ¿Qué header es necesario para enviar JSON en un POST?
a) Accept: application/json
b) Content-Type: application/json
c) Authorization: Bearer
d) X-JSON: true

4. ¿Cuántas veces se puede leer el body de un Response?
a) Infinitas
b) Dos
c) Una sola vez
d) Depende del tamaño

5. ¿Para qué sirve debounce en una búsqueda?
a) Para hacer la búsqueda más rápida
b) Para esperar a que el usuario deje de escribir antes de buscar
c) Para cachear resultados
d) Para ordenar resultados

Respuestas: 1-b, 2-b, 3-b, 4-c, 5-b


💡 Concepto Clave

Revisemos los puntos más importantes de esta lección antes de continuar.

Ejercicio práctico

Misión: Buscador de países con Fetch API

Creá una página web completa que:

  1. Tenga un campo de búsqueda con debounce (300ms)
  2. Busque países usando https://restcountries.com/v3.1/name/{nombre}
  3. Muestre los resultados como tarjetas con: bandera, nombre, capital, población, región
  4. Tenga un estado de "cargando" mientras busca
  5. Muestre un mensaje amigable si no hay resultados
  6. Al hacer click en un país, muestre información detallada en un modal
  7. Maneje errores de red con retry automático

APIs sugeridas para practicar:

  • Países: https://restcountries.com/v3.1/
  • Pokemon: https://pokeapi.co/api/v2/
  • Recetas: https://www.themealdb.com/api/json/v1/1/
  • Clima: https://api.open-meteo.com/v1/forecast
🧠 Pon a prueba tu conocimiento
¿Cuál es el aspecto más importante que aprendiste en esta lección?
  • Comprendo el concepto principal y puedo explicarlo con mis palabras
  • Entiendo cómo aplicarlo en mi situación específica
  • Necesito repasar algunas partes antes de continuar
  • Quiero ver más ejemplos prácticos del tema
✅ ¡Excelente! Continúa con la siguiente lección para profundizar más.