API Fetch

Avanzado
JavaScript
JavaScript
Actualizado: 19/05/2025

¡Desbloquea el curso de JavaScript completo!

IA
Ejercicios
Certificado
Entrar

Mira la lección en vídeo

Accede al vídeo completo de esta lección y a más contenido exclusivo con el Plan Plus.

Desbloquear Plan Plus

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

Guarda tu progreso

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

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.

Aprendizajes de esta lección de JavaScript

  • 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.

Completa este curso de JavaScript y certifícate

Únete a nuestra plataforma de cursos de programación y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración