JavaScript
Tutorial JavaScript: API Fetch
Aprende a configurar peticiones, procesar respuestas y manejar CORS, autenticación y progreso con la API Fetch en JavaScript.
Aprende JavaScript y certifícateConfiguración de peticiones: Métodos HTTP, headers y cuerpo de la solicitud
La API Fetch proporciona una interfaz moderna para realizar peticiones HTTP en JavaScript. Una de sus principales ventajas es la capacidad de configurar detalladamente cada solicitud según nuestras necesidades específicas. Vamos a explorar cómo personalizar nuestras peticiones mediante métodos HTTP, cabeceras y el cuerpo de la solicitud.
Estructura básica de una petición Fetch
Antes de profundizar en las opciones de configuración, veamos la estructura básica de una petición con Fetch:
fetch(url, options)
.then(response => {
// Procesamiento de la respuesta
})
.catch(error => {
// Manejo de errores
});
El parámetro options
es un objeto de configuración que nos permite personalizar nuestra petición. Si no lo proporcionamos, Fetch realizará una petición GET simple.
Métodos HTTP
Los métodos HTTP definen la acción que queremos realizar sobre el recurso. Fetch soporta todos los métodos estándar a través de la propiedad method
:
// Petición GET (método por defecto)
fetch('https://api.ejemplo.com/productos');
// Petición POST
fetch('https://api.ejemplo.com/productos', {
method: 'POST'
});
Los métodos HTTP más comunes que podemos utilizar son:
- GET: Solicita datos de un recurso específico (método por defecto).
- POST: Envía datos para crear un nuevo recurso.
- PUT: Actualiza completamente un recurso existente.
- PATCH: Actualiza parcialmente un recurso existente.
- DELETE: Elimina un recurso específico.
- HEAD: Similar a GET pero solo solicita las cabeceras (sin cuerpo).
- OPTIONS: Obtiene los métodos HTTP permitidos para un recurso.
Veamos un ejemplo práctico de cómo usar diferentes métodos:
// Obtener lista de usuarios
fetch('https://api.ejemplo.com/usuarios');
// Crear un nuevo usuario
fetch('https://api.ejemplo.com/usuarios', {
method: 'POST'
});
// Actualizar un usuario existente
fetch('https://api.ejemplo.com/usuarios/123', {
method: 'PUT'
});
// Eliminar un usuario
fetch('https://api.ejemplo.com/usuarios/123', {
method: 'DELETE'
});
Configuración de headers (cabeceras)
Las cabeceras HTTP permiten enviar información adicional con nuestra petición. Se configuran mediante la propiedad headers
, que acepta un objeto o una instancia de la clase Headers
:
// Usando un objeto simple
fetch('https://api.ejemplo.com/datos', {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
}
});
// Usando la interfaz Headers
const cabeceras = new Headers();
cabeceras.append('Content-Type', 'application/json');
cabeceras.append('Authorization', 'Bearer token123');
fetch('https://api.ejemplo.com/datos', {
headers: cabeceras
});
La interfaz Headers
proporciona métodos útiles para manipular cabeceras:
const cabeceras = new Headers();
// Añadir una cabecera
cabeceras.append('Accept-Language', 'es-ES');
// Comprobar si existe una cabecera
if (cabeceras.has('Accept-Language')) {
console.log('Cabecera de idioma configurada');
}
// Obtener el valor de una cabecera
console.log(cabeceras.get('Accept-Language')); // 'es-ES'
// Establecer una cabecera (reemplaza si ya existe)
cabeceras.set('Accept-Language', 'en-US');
// Eliminar una cabecera
cabeceras.delete('Accept-Language');
Cabeceras comunes
Algunas de las cabeceras más utilizadas en las peticiones son:
- Content-Type: Especifica el formato de los datos enviados.
- Authorization: Proporciona credenciales para la autenticación.
- Accept: Indica los formatos de respuesta que el cliente puede procesar.
- Accept-Language: Especifica los idiomas preferidos para la respuesta.
- User-Agent: Identifica el cliente que realiza la petición.
- Cache-Control: Define directivas de caché para la petición/respuesta.
fetch('https://api.ejemplo.com/datos', {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Accept-Language': 'es-ES,es;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache'
}
});
Configuración del cuerpo de la solicitud (body)
El cuerpo de la solicitud contiene los datos que queremos enviar al servidor. Se configura mediante la propiedad body
y solo es aplicable a métodos como POST, PUT o PATCH:
fetch('https://api.ejemplo.com/usuarios', {
method: 'POST',
body: JSON.stringify({
nombre: 'Ana',
email: 'ana@ejemplo.com'
}),
headers: {
'Content-Type': 'application/json'
}
});
Fetch acepta diferentes tipos de datos en el cuerpo:
- String: Texto plano o JSON serializado.
- FormData: Para enviar datos de formulario.
- Blob/File: Para enviar archivos binarios.
- ArrayBuffer: Para datos binarios de bajo nivel.
- URLSearchParams: Para datos codificados como URL.
Veamos ejemplos de cada uno:
Envío de JSON
const usuario = {
nombre: 'Carlos',
edad: 28,
intereses: ['programación', 'música']
};
fetch('https://api.ejemplo.com/usuarios', {
method: 'POST',
body: JSON.stringify(usuario),
headers: {
'Content-Type': 'application/json'
}
});
Envío de datos de formulario
// Creando un FormData a partir de un formulario existente
const formulario = document.querySelector('#formulario-registro');
const formData = new FormData(formulario);
// O creando y añadiendo campos manualmente
const formData = new FormData();
formData.append('nombre', 'Laura');
formData.append('email', 'laura@ejemplo.com');
formData.append('foto', fileInput.files[0]); // Añadir un archivo
fetch('https://api.ejemplo.com/registro', {
method: 'POST',
body: formData
// No es necesario establecer Content-Type, se configura automáticamente
});
Envío de archivos
const fileInput = document.querySelector('#input-archivo');
const archivo = fileInput.files[0];
fetch('https://api.ejemplo.com/upload', {
method: 'POST',
body: archivo,
headers: {
'Content-Type': archivo.type
}
});
Envío de parámetros URL
const params = new URLSearchParams();
params.append('q', 'javascript');
params.append('sort', 'relevance');
// Para peticiones GET, los parámetros van en la URL
fetch(`https://api.ejemplo.com/buscar?${params}`);
// Para POST, pueden ir en el cuerpo
fetch('https://api.ejemplo.com/buscar', {
method: 'POST',
body: params,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
Opciones adicionales de configuración
Además de method
, headers
y body
, Fetch acepta otras opciones importantes:
- mode: Controla si la petición puede realizarse a otros dominios.
- credentials: Determina si se envían cookies con la petición.
- cache: Controla cómo interactúa la petición con la caché del navegador.
- redirect: Especifica cómo manejar las redirecciones.
- referrer: Controla el valor de la cabecera Referer.
- integrity: Permite verificar que el recurso no ha sido manipulado.
fetch('https://api.ejemplo.com/datos', {
method: 'GET',
mode: 'cors',
credentials: 'include',
cache: 'no-cache',
redirect: 'follow',
referrer: 'https://miaplicacion.com',
integrity: 'sha256-abcdef1234567890'
});
Ejemplo completo: Actualización de perfil de usuario
Veamos un ejemplo práctico que combina varios aspectos de la configuración de peticiones:
async function actualizarPerfil(userId, datosUsuario, token) {
try {
const respuesta = await fetch(`https://api.ejemplo.com/usuarios/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
'Accept-Language': 'es-ES'
},
body: JSON.stringify(datosUsuario),
credentials: 'include',
mode: 'cors'
});
if (!respuesta.ok) {
throw new Error(`Error HTTP: ${respuesta.status}`);
}
return await respuesta.json();
} catch (error) {
console.error('Error al actualizar perfil:', error);
throw error;
}
}
// Uso de la función
const datosActualizados = {
nombre: 'Elena García',
email: 'elena@ejemplo.com',
preferencias: {
notificaciones: true,
tema: 'oscuro'
}
};
actualizarPerfil('user123', datosActualizados, 'mi-token-jwt')
.then(datos => console.log('Perfil actualizado:', datos))
.catch(error => console.error('Falló la actualización:', error));
Este ejemplo muestra una función completa para actualizar el perfil de un usuario, incluyendo:
- Método HTTP PUT para actualizar un recurso existente
- Cabeceras para especificar formato JSON, autenticación y preferencia de idioma
- Cuerpo de la solicitud con los datos del usuario serializados
- Configuración adicional para incluir cookies y permitir CORS
- Manejo adecuado de errores y respuestas
La configuración adecuada de las peticiones Fetch es fundamental para interactuar correctamente con APIs y servicios web, permitiéndonos adaptar cada solicitud a los requisitos específicos del servidor y optimizar la comunicación cliente-servidor.
Procesamiento de respuestas: Métodos para extraer y transformar datos
Cuando realizamos una petición con la API Fetch, recibimos un objeto Response
que representa la respuesta del servidor. Este objeto no contiene directamente los datos que solicitamos, sino que proporciona métodos específicos para extraer y transformar la información según el formato en que se encuentre.
El objeto Response
Antes de procesar los datos, es importante entender las propiedades principales del objeto Response
:
fetch('https://api.ejemplo.com/datos')
.then(response => {
console.log(response.status); // Código de estado HTTP (200, 404, etc.)
console.log(response.ok); // true si el status está entre 200-299
console.log(response.headers); // Objeto Headers con las cabeceras de respuesta
console.log(response.url); // URL completa de la respuesta
console.log(response.type); // Tipo de respuesta (basic, cors, etc.)
console.log(response.redirected); // Indica si hubo redirecciones
});
Estas propiedades nos permiten verificar el estado de la respuesta antes de procesar su contenido. La práctica recomendada es comprobar si la respuesta fue exitosa:
fetch('https://api.ejemplo.com/datos')
.then(response => {
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`);
}
return response.json(); // Continuamos con el procesamiento
})
.then(datos => {
// Trabajamos con los datos
})
.catch(error => {
console.error('Problema con la petición fetch:', error);
});
Métodos de extracción de datos
El objeto Response
proporciona varios métodos de extracción que devuelven promesas y transforman el cuerpo de la respuesta al formato deseado:
- json(): Interpreta el cuerpo como JSON
- text(): Extrae el cuerpo como texto plano
- blob(): Devuelve el cuerpo como un objeto Blob (datos binarios)
- arrayBuffer(): Convierte el cuerpo a ArrayBuffer (representación de bajo nivel)
- formData(): Interpreta el cuerpo como datos de formulario
Es importante destacar que solo se puede utilizar un método de extracción por respuesta. Esto se debe a que estos métodos consumen el cuerpo de la respuesta, que es un flujo de datos que solo puede leerse una vez.
Procesando respuestas JSON
El formato más común en las APIs modernas es JSON. Para procesar este tipo de respuestas utilizamos el método json()
:
fetch('https://api.ejemplo.com/usuarios')
.then(response => {
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
return response.json();
})
.then(usuarios => {
// usuarios es ya un objeto JavaScript
console.log(`Se encontraron ${usuarios.length} usuarios`);
// Podemos trabajar con los datos directamente
const nombresUsuarios = usuarios.map(usuario => usuario.nombre);
console.log('Nombres:', nombresUsuarios);
// O filtrar según criterios
const usuariosActivos = usuarios.filter(usuario => usuario.activo);
console.log('Usuarios activos:', usuariosActivos);
})
.catch(error => console.error('Error al procesar usuarios:', error));
El método json()
analiza el texto de la respuesta como JSON y lo transforma en el objeto o array JavaScript equivalente, permitiéndonos manipular los datos con facilidad.
Procesando respuestas de texto
Para respuestas en formato de texto plano (HTML, XML, CSV, etc.), utilizamos el método text()
:
fetch('https://ejemplo.com/articulo.html')
.then(response => {
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
return response.text();
})
.then(html => {
// Trabajamos con el contenido como string
console.log('Longitud del HTML:', html.length);
// Podemos insertar el HTML en el DOM
document.getElementById('contenedor').innerHTML = html;
// O procesarlo con expresiones regulares
const titulo = html.match(/<title>(.*?)<\/title>/i);
if (titulo && titulo[1]) {
console.log('Título de la página:', titulo[1]);
}
})
.catch(error => console.error('Error al obtener el texto:', error));
El método text()
es útil cuando necesitamos procesar datos no estructurados o cuando queremos realizar nuestro propio análisis del contenido.
Procesando datos binarios con Blob
Para manejar datos binarios como imágenes, archivos PDF o cualquier otro contenido no textual, utilizamos el método blob()
:
fetch('https://ejemplo.com/imagen.jpg')
.then(response => {
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
return response.blob();
})
.then(blob => {
// Creamos una URL para el blob
const url = URL.createObjectURL(blob);
// Podemos usar la URL para mostrar la imagen
const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);
// O crear un enlace de descarga
const enlace = document.createElement('a');
enlace.href = url;
enlace.download = 'imagen_descargada.jpg';
enlace.textContent = 'Descargar imagen';
document.body.appendChild(enlace);
// Es buena práctica revocar la URL cuando ya no se necesite
// para liberar memoria
setTimeout(() => URL.revokeObjectURL(url), 60000);
})
.catch(error => console.error('Error al procesar la imagen:', error));
El objeto Blob
(Binary Large Object) representa datos binarios y es ideal para manejar archivos multimedia o documentos.
Trabajando con ArrayBuffer
Para operaciones de bajo nivel con datos binarios, podemos utilizar arrayBuffer()
:
fetch('https://ejemplo.com/datos.bin')
.then(response => {
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
return response.arrayBuffer();
})
.then(buffer => {
// Creamos una vista de los datos
const view = new Uint8Array(buffer);
// Podemos analizar los bytes
let suma = 0;
for (let i = 0; i < view.length; i++) {
suma += view[i];
}
console.log('Suma de todos los bytes:', suma);
// O convertir a otros formatos
const decoder = new TextDecoder('utf-8');
const texto = decoder.decode(buffer);
console.log('Texto decodificado:', texto);
})
.catch(error => console.error('Error al procesar el buffer:', error));
ArrayBuffer
es útil cuando necesitamos acceso directo a los bytes de la respuesta, como en aplicaciones de procesamiento de audio, vídeo o cuando implementamos protocolos binarios personalizados.
Procesando datos de formulario
El método formData()
es útil cuando la respuesta contiene datos en formato multipart/form-data
:
fetch('https://api.ejemplo.com/formulario')
.then(response => {
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
return response.formData();
})
.then(formData => {
// Iteramos por todos los campos
for (const [clave, valor] of formData.entries()) {
console.log(`${clave}: ${valor}`);
}
// Podemos obtener valores específicos
const nombre = formData.get('nombre');
console.log('Nombre:', nombre);
// O comprobar si existe un campo
if (formData.has('email')) {
console.log('Email:', formData.get('email'));
}
// También podemos modificar el FormData
formData.append('timestamp', Date.now());
// Y enviarlo en otra petición
return fetch('https://api.ejemplo.com/procesar', {
method: 'POST',
body: formData
});
})
.catch(error => console.error('Error al procesar el formulario:', error));
Transformación y manipulación de datos
Una vez extraídos los datos, podemos transformarlos según nuestras necesidades:
fetch('https://api.ejemplo.com/productos')
.then(response => response.json())
.then(productos => {
// Transformación básica con map
const precios = productos.map(producto => ({
id: producto.id,
nombre: producto.nombre,
precioConIVA: producto.precio * 1.21
}));
// Agrupación de datos
const porCategoria = productos.reduce((acc, producto) => {
if (!acc[producto.categoria]) {
acc[producto.categoria] = [];
}
acc[producto.categoria].push(producto);
return acc;
}, {});
// Cálculos estadísticos
const precioTotal = productos.reduce((sum, producto) => sum + producto.precio, 0);
const precioPromedio = precioTotal / productos.length;
console.log('Productos con IVA:', precios);
console.log('Agrupados por categoría:', porCategoria);
console.log('Precio promedio:', precioPromedio);
})
.catch(error => console.error('Error:', error));
Encadenamiento de transformaciones
Podemos encadenar múltiples transformaciones para procesar datos complejos:
fetch('https://api.ejemplo.com/ventas')
.then(response => response.json())
.then(ventas => {
// Paso 1: Normalizar fechas y valores
return ventas.map(venta => ({
...venta,
fecha: new Date(venta.fecha),
total: parseFloat(venta.total)
}));
})
.then(ventasNormalizadas => {
// Paso 2: Filtrar por período
const inicio = new Date('2023-01-01');
const fin = new Date('2023-12-31');
return ventasNormalizadas.filter(venta =>
venta.fecha >= inicio && venta.fecha <= fin
);
})
.then(ventasFiltradas => {
// Paso 3: Agrupar por mes
const porMes = ventasFiltradas.reduce((acc, venta) => {
const mes = venta.fecha.getMonth();
if (!acc[mes]) acc[mes] = [];
acc[mes].push(venta);
return acc;
}, {});
// Paso 4: Calcular totales mensuales
const totalesMensuales = Object.entries(porMes).map(([mes, ventas]) => ({
mes: parseInt(mes) + 1, // Los meses en JS van de 0 a 11
total: ventas.reduce((sum, venta) => sum + venta.total, 0),
cantidad: ventas.length
}));
return totalesMensuales;
})
.then(resultado => {
console.log('Análisis de ventas por mes:', resultado);
// Podríamos continuar con más transformaciones...
return resultado;
})
.catch(error => console.error('Error en el procesamiento:', error));
Conversión entre formatos
A veces necesitamos convertir entre diferentes formatos de datos:
// Convertir JSON a Blob (útil para descargar datos como archivo)
fetch('https://api.ejemplo.com/datos')
.then(response => response.json())
.then(datos => {
// Convertimos el objeto a una cadena JSON
const jsonString = JSON.stringify(datos, null, 2);
// Creamos un Blob con el JSON formateado
const blob = new Blob([jsonString], { type: 'application/json' });
// Creamos un enlace de descarga
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'datos.json';
a.textContent = 'Descargar JSON';
document.body.appendChild(a);
});
// Convertir texto CSV a array de objetos
fetch('https://ejemplo.com/datos.csv')
.then(response => response.text())
.then(csv => {
// Dividimos por líneas y obtenemos las cabeceras
const lineas = csv.split('\n');
const cabeceras = lineas[0].split(',');
// Convertimos cada línea en un objeto
const objetos = lineas.slice(1).map(linea => {
const valores = linea.split(',');
return cabeceras.reduce((obj, cabecera, i) => {
obj[cabecera.trim()] = valores[i]?.trim();
return obj;
}, {});
});
console.log('CSV convertido a objetos:', objetos);
});
Manejo de respuestas en caché
Fetch nos permite implementar estrategias de caché para optimizar el rendimiento:
// Función para obtener datos con caché
function obtenerConCache(url, tiempoCache = 60000) {
// Verificamos si tenemos una versión en caché
const cacheKey = `cache_${url}`;
const cachedData = localStorage.getItem(cacheKey);
if (cachedData) {
const { timestamp, data } = JSON.parse(cachedData);
// Comprobamos si el caché aún es válido
if (Date.now() - timestamp < tiempoCache) {
console.log('Usando datos en caché para:', url);
return Promise.resolve(data);
}
}
// Si no hay caché o expiró, hacemos la petición
return fetch(url)
.then(response => response.json())
.then(data => {
// Guardamos en caché
const cacheEntry = {
timestamp: Date.now(),
data
};
localStorage.setItem(cacheKey, JSON.stringify(cacheEntry));
return data;
});
}
// Uso
obtenerConCache('https://api.ejemplo.com/datos')
.then(datos => console.log('Datos obtenidos:', datos));
El procesamiento eficiente de respuestas es fundamental para crear aplicaciones web robustas. Dominar los diferentes métodos de extracción y técnicas de transformación de datos te permitirá manejar cualquier tipo de respuesta que recibas de una API o servicio web.
Casos prácticos: Manejo de CORS, autenticación y monitoreo del progreso de carga
Al trabajar con la API Fetch en aplicaciones reales, nos encontramos con desafíos prácticos que van más allá de las peticiones básicas. Tres de los escenarios más comunes son el manejo de políticas de CORS, la implementación de autenticación y el seguimiento del progreso de carga. Vamos a explorar soluciones efectivas para cada uno de estos casos.
Manejo de CORS (Cross-Origin Resource Sharing)
El CORS es un mecanismo de seguridad implementado por los navegadores que restringe las peticiones HTTP realizadas desde un origen (dominio) a otro diferente. Cuando intentamos acceder a recursos en dominios distintos al de nuestra aplicación, podemos encontrarnos con errores como:
Access to fetch at 'https://api.otrodominio.com/datos' from origin 'https://miapp.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.
Configuración del modo CORS en Fetch
Fetch nos permite especificar cómo queremos manejar las peticiones cross-origin mediante la propiedad mode
:
fetch('https://api.otrodominio.com/datos', {
mode: 'cors' // Valor por defecto
})
Las opciones disponibles para mode
son:
- cors: Permite peticiones cross-origin con las restricciones de CORS (predeterminado).
- no-cors: Limita la respuesta pero evita errores CORS (útil para recursos como imágenes).
- same-origin: Rechaza cualquier petición a otros orígenes.
- navigate: Reservado para navegación del navegador.
Soluciones prácticas para problemas de CORS
- Solicitar cambios en el servidor API
La solución ideal es que el servidor al que hacemos la petición configure correctamente los encabezados CORS:
Access-Control-Allow-Origin: https://miapp.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
- Usar un proxy en desarrollo
Durante el desarrollo, podemos configurar un servidor proxy local:
// En create-react-app, configurar en package.json:
{
"proxy": "https://api.otrodominio.com"
}
// Luego las peticiones se hacen a rutas relativas
fetch('/datos') // Internamente va a https://api.otrodominio.com/datos
- Implementar un proxy en producción
En producción, podemos crear un endpoint en nuestro servidor que actúe como intermediario:
// Frontend
fetch('/api/proxy/datos')
.then(response => response.json())
.then(data => console.log(data));
// Servidor (Node.js con Express)
app.get('/api/proxy/datos', async (req, res) => {
try {
const apiResponse = await fetch('https://api.otrodominio.com/datos');
const data = await apiResponse.json();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
- Manejo de preflight requests
Para peticiones complejas (con cabeceras personalizadas, métodos no simples), el navegador realiza una petición OPTIONS previa llamada "preflight":
// Esta petición desencadenará un preflight
fetch('https://api.otrodominio.com/usuarios', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'valor'
},
body: JSON.stringify({ nombre: 'Ana' })
});
El servidor debe responder correctamente a estas peticiones OPTIONS con los encabezados CORS apropiados.
Implementación de autenticación
La autenticación es fundamental para proteger recursos y personalizar experiencias. Fetch nos permite implementar diferentes estrategias de autenticación.
Autenticación con tokens JWT
Los tokens JWT (JSON Web Tokens) son una forma popular de autenticación en APIs modernas:
// Función para obtener datos autenticados
async function obtenerDatosProtegidos(url) {
// Recuperar token del almacenamiento local
const token = localStorage.getItem('authToken');
if (!token) {
// Redirigir a login si no hay token
window.location.href = '/login';
return;
}
try {
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
// Si el token ha expirado o es inválido
if (response.status === 401) {
// Limpiar token y redirigir a login
localStorage.removeItem('authToken');
window.location.href = '/login';
return;
}
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error de autenticación:', error);
throw error;
}
}
// Uso
obtenerDatosProtegidos('https://api.ejemplo.com/perfil')
.then(perfil => {
console.log('Datos del perfil:', perfil);
})
.catch(error => {
// Manejar errores
});
Renovación automática de tokens
Para mejorar la experiencia de usuario, podemos implementar la renovación automática de tokens:
// Clase para gestionar la autenticación
class AuthService {
constructor() {
this.tokenKey = 'authToken';
this.refreshTokenKey = 'refreshToken';
this.apiUrl = 'https://api.ejemplo.com';
}
getToken() {
return localStorage.getItem(this.tokenKey);
}
getRefreshToken() {
return localStorage.getItem(this.refreshTokenKey);
}
saveTokens(token, refreshToken) {
localStorage.setItem(this.tokenKey, token);
localStorage.setItem(this.refreshTokenKey, refreshToken);
}
clearTokens() {
localStorage.removeItem(this.tokenKey);
localStorage.removeItem(this.refreshTokenKey);
}
async refreshAuth() {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
throw new Error('No hay refresh token disponible');
}
const response = await fetch(`${this.apiUrl}/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken })
});
if (!response.ok) {
this.clearTokens();
throw new Error('No se pudo renovar la autenticación');
}
const { token, refreshToken: newRefreshToken } = await response.json();
this.saveTokens(token, newRefreshToken);
return token;
}
async fetchWithAuth(url, options = {}) {
// Preparar opciones con token
const authOptions = {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.getToken()}`
}
};
// Realizar petición
let response = await fetch(url, authOptions);
// Si hay error de autenticación, intentar renovar token
if (response.status === 401) {
try {
await this.refreshAuth();
// Repetir petición con nuevo token
authOptions.headers['Authorization'] = `Bearer ${this.getToken()}`;
response = await fetch(url, authOptions);
} catch (error) {
// Si falla la renovación, redirigir a login
window.location.href = '/login';
throw error;
}
}
return response;
}
}
// Uso
const auth = new AuthService();
auth.fetchWithAuth('https://api.ejemplo.com/datos')
.then(response => response.json())
.then(data => console.log('Datos protegidos:', data))
.catch(error => console.error('Error:', error));
Autenticación con cookies
Si la API utiliza cookies para la autenticación, debemos configurar credentials
:
fetch('https://api.ejemplo.com/perfil', {
credentials: 'include' // Incluye cookies en peticiones cross-origin
})
.then(response => response.json())
.then(perfil => console.log(perfil));
Las opciones para credentials
son:
- omit: No incluye cookies (predeterminado).
- same-origin: Incluye cookies solo para el mismo origen.
- include: Incluye cookies para todos los orígenes (requiere configuración CORS en el servidor).
Monitoreo del progreso de carga
Para archivos grandes o conexiones lentas, es importante proporcionar feedback sobre el progreso de carga. Fetch no ofrece esta funcionalidad directamente, pero podemos implementarla combinándolo con la API XMLHttpRequest
o utilizando la más moderna API ReadableStream
.
Monitoreo con XMLHttpRequest
function fetchConProgreso(url, opciones = {}, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// Configurar el evento de progreso
xhr.upload.onprogress = (event) => {
if (event.lengthComputable && onProgress) {
const porcentaje = Math.round((event.loaded / event.total) * 100);
onProgress(porcentaje, event);
}
};
// Configurar eventos de finalización
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
ok: true,
status: xhr.status,
json: () => JSON.parse(xhr.responseText),
text: () => Promise.resolve(xhr.responseText)
});
} else {
reject(new Error(`HTTP Error: ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('Error de red'));
// Abrir y enviar la petición
xhr.open(opciones.method || 'GET', url);
// Configurar cabeceras
if (opciones.headers) {
Object.keys(opciones.headers).forEach(key => {
xhr.setRequestHeader(key, opciones.headers[key]);
});
}
// Enviar datos o vacío
xhr.send(opciones.body || null);
});
}
// Ejemplo de uso para subir un archivo
const fileInput = document.querySelector('#archivo');
const progressBar = document.querySelector('#progreso');
const statusText = document.querySelector('#estado');
fileInput.addEventListener('change', async (e) => {
if (!e.target.files.length) return;
const archivo = e.target.files[0];
const formData = new FormData();
formData.append('archivo', archivo);
try {
statusText.textContent = 'Subiendo archivo...';
await fetchConProgreso(
'https://api.ejemplo.com/upload',
{
method: 'POST',
body: formData
},
(porcentaje) => {
progressBar.value = porcentaje;
statusText.textContent = `Subiendo: ${porcentaje}%`;
}
);
statusText.textContent = '¡Archivo subido con éxito!';
} catch (error) {
statusText.textContent = `Error: ${error.message}`;
console.error('Error al subir:', error);
}
});
Monitoreo de descarga con ReadableStream
Para monitorear el progreso de descarga, podemos usar la API ReadableStream
:
async function descargarConProgreso(url, onProgress) {
// Realizar la petición
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`);
}
// Obtener el tamaño total si está disponible
const contentLength = response.headers.get('Content-Length');
const total = contentLength ? parseInt(contentLength, 10) : 0;
let loaded = 0;
// Crear un nuevo lector para el stream
const reader = response.body.getReader();
// Función para procesar los chunks de datos
const processStream = async () => {
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
chunks.push(value);
loaded += value.length;
// Calcular y reportar el progreso
if (total && onProgress) {
const porcentaje = Math.round((loaded / total) * 100);
onProgress(porcentaje, { loaded, total });
}
}
// Combinar todos los chunks en un solo Uint8Array
const chunksAll = new Uint8Array(loaded);
let position = 0;
for (const chunk of chunks) {
chunksAll.set(chunk, position);
position += chunk.length;
}
return chunksAll;
};
// Procesar el stream y devolver los datos
const data = await processStream();
// Convertir según el tipo de contenido
const contentType = response.headers.get('Content-Type') || '';
if (contentType.includes('application/json')) {
const decoder = new TextDecoder('utf-8');
const text = decoder.decode(data);
return JSON.parse(text);
} else if (contentType.includes('text/')) {
const decoder = new TextDecoder('utf-8');
return decoder.decode(data);
} else {
// Devolver como blob para archivos binarios
return new Blob([data], { type: contentType });
}
}
// Ejemplo de uso
const progressElement = document.querySelector('#progreso-descarga');
const statusElement = document.querySelector('#estado-descarga');
async function descargarArchivo() {
try {
statusElement.textContent = 'Descargando archivo...';
const blob = await descargarConProgreso(
'https://ejemplo.com/archivo-grande.zip',
(porcentaje, { loaded, total }) => {
progressElement.value = porcentaje;
statusElement.textContent = `Descargando: ${porcentaje}% (${(loaded / 1048576).toFixed(2)} MB / ${(total / 1048576).toFixed(2)} MB)`;
}
);
// Crear URL y enlace de descarga
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'archivo-descargado.zip';
a.textContent = 'Guardar archivo';
document.body.appendChild(a);
statusElement.textContent = '¡Descarga completada!';
// Limpiar la URL cuando ya no se necesite
setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch (error) {
statusElement.textContent = `Error: ${error.message}`;
console.error('Error en la descarga:', error);
}
}
// Iniciar descarga al hacer clic en un botón
document.querySelector('#btn-descargar').addEventListener('click', descargarArchivo);
Combinando todo: Aplicación de galería de imágenes
Veamos un ejemplo completo que combina CORS, autenticación y monitoreo de progreso en una aplicación de galería de imágenes:
class GaleriaApp {
constructor() {
this.apiUrl = 'https://api.galeria.com';
this.authService = new AuthService();
this.galeriaElement = document.querySelector('#galeria');
this.uploadForm = document.querySelector('#upload-form');
this.progressElement = document.querySelector('#progress');
this.statusElement = document.querySelector('#status');
this.init();
}
async init() {
// Comprobar autenticación
if (!this.authService.getToken()) {
window.location.href = '/login';
return;
}
// Configurar eventos
this.uploadForm.addEventListener('submit', this.handleUpload.bind(this));
// Cargar imágenes
await this.cargarImagenes();
}
async cargarImagenes() {
try {
this.statusElement.textContent = 'Cargando imágenes...';
const response = await this.authService.fetchWithAuth(`${this.apiUrl}/imagenes`);
if (!response.ok) {
throw new Error(`Error al cargar imágenes: ${response.status}`);
}
const imagenes = await response.json();
// Limpiar galería
this.galeriaElement.innerHTML = '';
// Mostrar imágenes
imagenes.forEach(imagen => {
const imgElement = document.createElement('img');
imgElement.src = imagen.thumbnailUrl;
imgElement.alt = imagen.titulo;
imgElement.dataset.id = imagen.id;
// Añadir evento para ver imagen completa
imgElement.addEventListener('click', () => this.verImagenCompleta(imagen.id));
this.galeriaElement.appendChild(imgElement);
});
this.statusElement.textContent = `${imagenes.length} imágenes cargadas`;
} catch (error) {
this.statusElement.textContent = `Error: ${error.message}`;
console.error('Error al cargar la galería:', error);
}
}
async verImagenCompleta(id) {
try {
this.statusElement.textContent = 'Cargando imagen...';
this.progressElement.value = 0;
this.progressElement.style.display = 'block';
// Descargar imagen con progreso
const blob = await descargarConProgreso(
`${this.apiUrl}/imagenes/${id}/original`,
(porcentaje) => {
this.progressElement.value = porcentaje;
this.statusElement.textContent = `Cargando imagen: ${porcentaje}%`;
},
{ headers: { 'Authorization': `Bearer ${this.authService.getToken()}` } }
);
// Crear URL para la imagen
const url = URL.createObjectURL(blob);
// Mostrar imagen en modal
const modal = document.createElement('div');
modal.className = 'modal';
const img = document.createElement('img');
img.src = url;
modal.appendChild(img);
document.body.appendChild(modal);
// Cerrar modal al hacer clic
modal.addEventListener('click', () => {
document.body.removeChild(modal);
URL.revokeObjectURL(url);
});
this.progressElement.style.display = 'none';
this.statusElement.textContent = 'Imagen cargada';
} catch (error) {
this.progressElement.style.display = 'none';
this.statusElement.textContent = `Error: ${error.message}`;
console.error('Error al cargar imagen:', error);
}
}
async handleUpload(event) {
event.preventDefault();
const fileInput = this.uploadForm.querySelector('input[type="file"]');
if (!fileInput.files.length) {
this.statusElement.textContent = 'Por favor, selecciona una imagen';
return;
}
const archivo = fileInput.files[0];
const formData = new FormData();
formData.append('imagen', archivo);
formData.append('titulo', this.uploadForm.querySelector('input[name="titulo"]').value);
try {
this.statusElement.textContent = 'Subiendo imagen...';
this.progressElement.value = 0;
this.progressElement.style.display = 'block';
// Subir con progreso
await fetchConProgreso(
`${this.apiUrl}/imagenes`,
{
method: 'POST',
body: formData,
headers: {
'Authorization': `Bearer ${this.authService.getToken()}`
}
},
(porcentaje) => {
this.progressElement.value = porcentaje;
this.statusElement.textContent = `Subiendo: ${porcentaje}%`;
}
);
this.progressElement.style.display = 'none';
this.statusElement.textContent = '¡Imagen subida con éxito!';
// Recargar galería
this.uploadForm.reset();
await this.cargarImagenes();
} catch (error) {
this.progressElement.style.display = 'none';
this.statusElement.textContent = `Error: ${error.message}`;
console.error('Error al subir imagen:', error);
}
}
}
// Iniciar aplicación cuando el DOM esté listo
document.addEventListener('DOMContentLoaded', () => {
new GaleriaApp();
});
Este ejemplo muestra cómo integrar los tres conceptos en una aplicación real:
- Manejo de CORS: A través del servicio de autenticación que gestiona las peticiones cross-origin.
- Autenticación: Implementando un sistema completo con tokens JWT y renovación automática.
- Monitoreo de progreso: Tanto para subidas como para descargas de archivos, proporcionando feedback visual al usuario.
Dominar estos casos prácticos te permitirá desarrollar aplicaciones web robustas que ofrezcan una excelente experiencia de usuario incluso en situaciones complejas como conexiones lentas o requisitos de seguridad estrictos.
Otras lecciones de JavaScript
Accede a todas las lecciones de JavaScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Javascript
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Funciones Cierre (Closure)
Sintaxis
Métodos De Strings
Sintaxis
Funciones Cierre (Closure)
Sintaxis
Operadores Avanzados
Sintaxis
Funciones
Sintaxis
Expresiones Regulares
Sintaxis
Estructuras De Control
Sintaxis
Arrays Y Métodos
Estructuras De Datos
Conjuntos Con Set
Estructuras De Datos
Mapas Con Map
Estructuras De Datos
Conjuntos Con Set
Estructuras De Datos
Funciones Flecha
Programación Funcional
Filtrado Con Filter() Y Find()
Programación Funcional
Transformación Con Map()
Programación Funcional
Reducción Con Reduce()
Programación Funcional
Funciones Flecha
Programación Funcional
Transformación Con Map()
Programación Funcional
Inmutabilidad Y Programación Funcional Pura
Programación Funcional
Clases Y Objetos
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
This Y Contexto
Programación Orientada A Objetos
Patrón De Módulos Y Namespace
Programación Orientada A Objetos
Prototipos Y Cadena De Prototipos
Programación Orientada A Objetos
Destructuring De Objetos Y Arrays
Programación Orientada A Objetos
Manipulación Dom
Dom
Selección De Elementos Dom
Dom
Modificación De Elementos Dom
Dom
Eventos Del Dom
Dom
Localstorage Y Sessionstorage
Dom
Bom (Browser Object Model)
Dom
Callbacks
Programación Asíncrona
Promises
Programación Asíncrona
Async / Await
Programación Asíncrona
Api Fetch
Programación Asíncrona
Naturaleza De Js Y Event Loop
Programación Asíncrona
Websockets
Programación Asíncrona
Módulos En Es6
Construcción
Configuración De Bundlers Como Vite
Construcción
Eslint Y Calidad De Código
Construcción
Npm Y Dependencias
Construcción
Introducción A Pruebas En Js
Testing
Pruebas Unitarias
Testing
Ejercicios de programación de JavaScript
Evalúa tus conocimientos de esta lección API Fetch con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Excepciones
Transformación con map()
Arrays y Métodos
Reto Métodos de Strings
Transformación con map()
Funciones flecha
Selección de elementos DOM
API Fetch
Encapsulación
Mapas con Map
Creación y uso de variables
Polimorfismo
Reto Funciones flecha
Tipos de datos
Reto Operadores avanzados
Reto Estructuras de control
Estructuras de control
Pruebas unitarias
Inmutabilidad y programación funcional pura
Funciones flecha
Polimorfismo
Reto Polimorfismo
Array
Transformación con map()
Reto Variables
Gestor de tareas con JavaScript
Proyecto Modificación de elementos DOM
Manipulación DOM
Funciones
Conjuntos con Set
Reto Prototipos y cadena de prototipos
Reto Encapsulación
Funciones flecha
Async / Await
Reto Excepciones
Reto Filtrado con filter() y find()
Reto Promises
Creación y uso de variables
Excepciones
Promises
Funciones cierre (closure)
Reto Herencia
Herencia
Reto Async / Await
Proyecto Eventos del DOM
Herencia
Selección de elementos DOM
Modificación de elementos DOM
Reto Clases y objetos
Filtrado con filter() y find()
Funciones cierre (closure)
Reto Destructuring de objetos y arrays
Callbacks
Funciones
Mapas con Map
Reducción con reduce()
Callbacks
Manipulación DOM
Introducción al DOM
Reto Funciones
Reto Funciones cierre (closure)
Promises
Reto Reducción con reduce()
Async / Await
Reto Estructuras de control
Eventos del DOM
Introducción a JavaScript
Async / Await
Promises
Selección de elementos DOM
Filtrado con filter() y find()
Callbacks
Creación de clases y objetos Restaurante
Reducción con reduce()
Filtrado con filter() y find()
Reducción con reduce()
Conjuntos con Set
Herencia de clases
Eventos del DOM
Clases y objetos
Modificación de elementos DOM
Mapas con Map
Proyecto carrito compra agoodshop
Introducción a JavaScript
Reto Mapas con Map
Funciones
Proyecto administrador de contactos
Reto Expresiones regulares
Tipos de datos
Clases y objetos
Array
Conjuntos con Set
Array
Encapsulación
Clases y objetos
Uso de operadores
Uso de operadores
Estructuras de control
Proyecto Manipulación DOM
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender cómo configurar peticiones HTTP con Fetch usando métodos, cabeceras y cuerpo.
- Aprender a extraer y transformar datos de respuestas usando los métodos del objeto Response.
- Conocer las técnicas para manejar problemas comunes como CORS y autenticación con tokens o cookies.
- Implementar el seguimiento del progreso de carga y descarga en peticiones Fetch.
- Integrar conceptos avanzados en aplicaciones reales para mejorar la comunicación cliente-servidor.