Fetch API: Consumiendo APIs del Mundo Real
Objetivos de aprendizajeAl 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
No verificar
response.ok:fetchNO rechaza en errores HTTP (404, 500). Solo rechaza en errores de red. Siempre verificáresponse.ok.Leer el body dos veces:
response.json()yresponse.text()solo se pueden llamar UNA vez. Guardá el resultado en una variable.Olvidar
JSON.stringify()en el body del POST: El body debe ser un string, no un objeto.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.
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
fetch()retorna una Promise con el objeto Response — siempre verificáresponse.ok.- Usá
response.json()para parsear JSON,response.text()para texto plano. - Para POST/PUT, necesitás:
method,headerscon Content-Type, ybodyconJSON.stringify(). Promise.all()con fetch permite cargar datos de múltiples APIs en paralelo.- Siempre manejá los tres estados: loading, success y error.
- Un ApiClient wrapper reutilizable simplifica mucho el trabajo con APIs.
- 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
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:
- Tenga un campo de búsqueda con debounce (300ms)
- Busque países usando
https://restcountries.com/v3.1/name/{nombre} - Muestre los resultados como tarjetas con: bandera, nombre, capital, población, región
- Tenga un estado de "cargando" mientras busca
- Muestre un mensaje amigable si no hay resultados
- Al hacer click en un país, muestre información detallada en un modal
- 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
- 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