JavaScript

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ícate

Configuració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

  1. 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
  1. 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
  1. 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 });
  }
});
  1. 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:

  1. Manejo de CORS: A través del servicio de autenticación que gestiona las peticiones cross-origin.
  2. Autenticación: Implementando un sistema completo con tokens JWT y renovación automática.
  3. 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.

Aprende JavaScript online

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

JavaScript

Introducción Y Entorno

Tipos De Datos

JavaScript

Sintaxis

Variables

JavaScript

Sintaxis

Operadores

JavaScript

Sintaxis

Estructuras De Control

JavaScript

Sintaxis

Funciones

JavaScript

Sintaxis

Funciones Cierre (Closure)

JavaScript

Sintaxis

Métodos De Strings

JavaScript

Sintaxis

Funciones Cierre (Closure)

JavaScript

Sintaxis

Operadores Avanzados

JavaScript

Sintaxis

Funciones

JavaScript

Sintaxis

Expresiones Regulares

JavaScript

Sintaxis

Estructuras De Control

JavaScript

Sintaxis

Arrays Y Métodos

JavaScript

Estructuras De Datos

Conjuntos Con Set

JavaScript

Estructuras De Datos

Mapas Con Map

JavaScript

Estructuras De Datos

Conjuntos Con Set

JavaScript

Estructuras De Datos

Funciones Flecha

JavaScript

Programación Funcional

Filtrado Con Filter() Y Find()

JavaScript

Programación Funcional

Transformación Con Map()

JavaScript

Programación Funcional

Reducción Con Reduce()

JavaScript

Programación Funcional

Funciones Flecha

JavaScript

Programación Funcional

Transformación Con Map()

JavaScript

Programación Funcional

Inmutabilidad Y Programación Funcional Pura

JavaScript

Programación Funcional

Clases Y Objetos

JavaScript

Programación Orientada A Objetos

Excepciones

JavaScript

Programación Orientada A Objetos

Encapsulación

JavaScript

Programación Orientada A Objetos

Herencia

JavaScript

Programación Orientada A Objetos

Polimorfismo

JavaScript

Programación Orientada A Objetos

This Y Contexto

JavaScript

Programación Orientada A Objetos

Patrón De Módulos Y Namespace

JavaScript

Programación Orientada A Objetos

Prototipos Y Cadena De Prototipos

JavaScript

Programación Orientada A Objetos

Destructuring De Objetos Y Arrays

JavaScript

Programación Orientada A Objetos

Manipulación Dom

JavaScript

Dom

Selección De Elementos Dom

JavaScript

Dom

Modificación De Elementos Dom

JavaScript

Dom

Eventos Del Dom

JavaScript

Dom

Localstorage Y Sessionstorage

JavaScript

Dom

Bom (Browser Object Model)

JavaScript

Dom

Callbacks

JavaScript

Programación Asíncrona

Promises

JavaScript

Programación Asíncrona

Async / Await

JavaScript

Programación Asíncrona

Api Fetch

JavaScript

Programación Asíncrona

Naturaleza De Js Y Event Loop

JavaScript

Programación Asíncrona

Websockets

JavaScript

Programación Asíncrona

Módulos En Es6

JavaScript

Construcción

Configuración De Bundlers Como Vite

JavaScript

Construcción

Eslint Y Calidad De Código

JavaScript

Construcción

Npm Y Dependencias

JavaScript

Construcción

Introducción A Pruebas En Js

JavaScript

Testing

Pruebas Unitarias

JavaScript

Testing

Accede GRATIS a JavaScript y certifícate

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

JavaScript
Test

Transformación con map()

JavaScript
Código

Arrays y Métodos

JavaScript
Código

Reto Métodos de Strings

JavaScript
Código

Transformación con map()

JavaScript
Puzzle

Funciones flecha

JavaScript
Test

Selección de elementos DOM

JavaScript
Puzzle

API Fetch

JavaScript
Código

Encapsulación

JavaScript
Test

Mapas con Map

JavaScript
Código

Creación y uso de variables

JavaScript
Puzzle

Polimorfismo

JavaScript
Puzzle

Reto Funciones flecha

JavaScript
Código

Tipos de datos

JavaScript
Puzzle

Reto Operadores avanzados

JavaScript
Código

Reto Estructuras de control

JavaScript
Código

Estructuras de control

JavaScript
Puzzle

Pruebas unitarias

JavaScript
Proyecto

Inmutabilidad y programación funcional pura

JavaScript
Código

Funciones flecha

JavaScript
Puzzle

Polimorfismo

JavaScript
Test

Reto Polimorfismo

JavaScript
Código

Array

JavaScript
Código

Transformación con map()

JavaScript
Test

Reto Variables

JavaScript
Código

Gestor de tareas con JavaScript

JavaScript
Proyecto

Proyecto Modificación de elementos DOM

JavaScript
Proyecto

Manipulación DOM

JavaScript
Test

Funciones

JavaScript
Test

Conjuntos con Set

JavaScript
Código

Reto Prototipos y cadena de prototipos

JavaScript
Código

Reto Encapsulación

JavaScript
Código

Funciones flecha

JavaScript
Código

Async / Await

JavaScript
Código

Reto Excepciones

JavaScript
Código

Reto Filtrado con filter() y find()

JavaScript
Código

Reto Promises

JavaScript
Código

Creación y uso de variables

JavaScript
Test

Excepciones

JavaScript
Puzzle

Promises

JavaScript
Código

Funciones cierre (closure)

JavaScript
Test

Reto Herencia

JavaScript
Código

Herencia

JavaScript
Puzzle

Reto Async / Await

JavaScript
Código

Proyecto Eventos del DOM

JavaScript
Proyecto

Herencia

JavaScript
Test

Selección de elementos DOM

JavaScript
Test

Modificación de elementos DOM

JavaScript
Test

Reto Clases y objetos

JavaScript
Código

Filtrado con filter() y find()

JavaScript
Test

Funciones cierre (closure)

JavaScript
Puzzle

Reto Destructuring de objetos y arrays

JavaScript
Código

Callbacks

JavaScript
Código

Funciones

JavaScript
Puzzle

Mapas con Map

JavaScript
Test

Reducción con reduce()

JavaScript
Test

Callbacks

JavaScript
Puzzle

Manipulación DOM

JavaScript
Puzzle

Introducción al DOM

JavaScript
Proyecto

Reto Funciones

JavaScript
Código

Reto Funciones cierre (closure)

JavaScript
Código

Promises

JavaScript
Test

Reto Reducción con reduce()

JavaScript
Código

Async / Await

JavaScript
Test

Reto Estructuras de control

JavaScript
Código

Eventos del DOM

JavaScript
Puzzle

Introducción a JavaScript

JavaScript
Puzzle

Async / Await

JavaScript
Puzzle

Promises

JavaScript
Puzzle

Selección de elementos DOM

JavaScript
Proyecto

Filtrado con filter() y find()

JavaScript
Código

Callbacks

JavaScript
Test

Creación de clases y objetos Restaurante

JavaScript
Código

Reducción con reduce()

JavaScript
Código

Filtrado con filter() y find()

JavaScript
Puzzle

Reducción con reduce()

JavaScript
Puzzle

Conjuntos con Set

JavaScript
Puzzle

Herencia de clases

JavaScript
Código

Eventos del DOM

JavaScript
Test

Clases y objetos

JavaScript
Puzzle

Modificación de elementos DOM

JavaScript
Puzzle

Mapas con Map

JavaScript
Puzzle

Proyecto carrito compra agoodshop

JavaScript
Proyecto

Introducción a JavaScript

JavaScript
Test

Reto Mapas con Map

JavaScript
Código

Funciones

JavaScript
Código

Proyecto administrador de contactos

JavaScript
Proyecto

Reto Expresiones regulares

JavaScript
Código

Tipos de datos

JavaScript
Test

Clases y objetos

JavaScript
Test

Array

JavaScript
Test

Conjuntos con Set

JavaScript
Test

Array

JavaScript
Puzzle

Encapsulación

JavaScript
Puzzle

Clases y objetos

JavaScript
Código

Uso de operadores

JavaScript
Puzzle

Uso de operadores

JavaScript
Test

Estructuras de control

JavaScript
Test

Proyecto Manipulación DOM

JavaScript
Proyecto

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.