Ir al contenido principal

Fetch API

Introducción a la API Fetch

La comunicación con servidores es una parte fundamental de las aplicaciones web modernas. Constantemente necesitamos enviar y recibir datos: obtener información de usuario, publicar contenido, cargar recursos o consumir servicios externos. Durante años, la forma estándar de realizar estas operaciones fue mediante el objeto XMLHttpRequest, pero en la actualidad disponemos de una alternativa más moderna, potente y fácil de usar: la API Fetch.

Fetch es una interfaz nativa de JavaScript para realizar peticiones HTTP de manera asíncrona. Diseñada como un reemplazo más potente y flexible para XMLHttpRequest, esta API facilita enormemente el consumo de recursos y la comunicación con servicios web, introduciendo un enfoque basado en promesas que se integra perfectamente con las características modernas de JavaScript.

En este artículo, exploraremos cómo utilizar Fetch para realizar diferentes tipos de peticiones HTTP, gestionar respuestas y aprovechar al máximo esta poderosa herramienta en nuestras aplicaciones web.

Sintaxis básica y opciones

La forma más simple de utilizar Fetch es pasando una URL como parámetro:

fetch('https://api.ejemplo.com/datos')
  .then(respuesta => {
    // Manejar la respuesta
    console.log('Respuesta recibida:', respuesta);
  })
  .catch(error => {
    // Manejar errores
    console.error('Error en la petición:', error);
  });

Este código realiza una petición GET básica a la URL especificada. Fetch devuelve una promesa que se resuelve con un objeto Response, que contiene la respuesta del servidor.

Sin embargo, Fetch acepta un segundo parámetro opcional que nos permite configurar diversos aspectos de la petición:

fetch('https://api.ejemplo.com/datos', {
  method: 'GET',                 // Método HTTP: GET, POST, PUT, DELETE, etc.
  headers: {                     // Cabeceras HTTP
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  mode: 'cors',                  // Modo CORS: cors, no-cors, same-origin
  cache: 'no-cache',             // Estrategia de caché
  credentials: 'same-origin',    // Incluir/excluir cookies: omit, same-origin, include
  redirect: 'follow',            // Manejo de redirecciones: follow, error, manual
  referrerPolicy: 'no-referrer', // Política de referrer
  body: JSON.stringify({         // Cuerpo de la petición (no válido para GET/HEAD)
    clave: 'valor',
    otroParametro: 42
  })
})
.then(respuesta => respuesta.json())
.then(datos => {
  console.log('Datos recibidos:', datos);
})
.catch(error => {
  console.error('Error:', error);
});

Veamos algunas de las opciones más importantes:

  • method: Especifica el método HTTP a utilizar. Por defecto es 'GET'.
  • headers: Objeto con las cabeceras HTTP para enviar con la petición.
  • body: El cuerpo de la petición. Puede ser una cadena, un objeto FormData, un Blob, etc.
  • credentials: Controla si se envían cookies con la petición.
  • mode: Controla cómo se manejan las peticiones entre diferentes orígenes (CORS).

Envío de solicitudes GET

Las solicitudes GET son las más comunes y se utilizan para obtener datos de un servidor. Por defecto, fetch realiza una petición GET si no especificamos el método:

// Función para obtener una lista de usuarios
async function obtenerUsuarios() {
  try {
    // Realizar la petición GET
    const respuesta = await fetch('https://api.ejemplo.com/usuarios');
    
    // Verificar si la respuesta fue exitosa
    if (!respuesta.ok) {
      throw new Error(`Error HTTP: ${respuesta.status}`);
    }
    
    // Convertir la respuesta a JSON
    const usuarios = await respuesta.json();
    
    // Procesar los datos
    console.log('Usuarios obtenidos:', usuarios);
    return usuarios;
  } catch (error) {
    console.error('Error al obtener usuarios:', error);
    throw error; // Propagar el error para manejo posterior
  }
}

// Uso de la función con async/await
async function mostrarUsuarios() {
  try {
    const usuarios = await obtenerUsuarios();
    // Ahora podemos trabajar con los usuarios
    usuarios.forEach(usuario => {
      console.log(`Nombre: ${usuario.nombre}, Email: ${usuario.email}`);
    });
  } catch (error) {
    // Manejar el error
    console.error('No se pudieron mostrar los usuarios:', error);
  }
}

Petición GET con parámetros de consulta

A menudo necesitamos incluir parámetros de consulta (query parameters) en nuestras URLs:

// Función para buscar usuarios según criterios
function buscarUsuarios(criterios) {
  // Construir la URL con parámetros de consulta
  const url = new URL('https://api.ejemplo.com/usuarios/buscar');
  
  // Añadir parámetros de búsqueda
  Object.keys(criterios).forEach(key => {
    url.searchParams.append(key, criterios[key]);
  });
  
  // Realizar la petición
  return fetch(url)
    .then(respuesta => {
      if (!respuesta.ok) throw new Error('Error en la búsqueda');
      return respuesta.json();
    });
}

// Ejemplo de uso
buscarUsuarios({
  nombre: 'Ana',
  ciudad: 'Madrid',
  edad_minima: 25
})
.then(resultados => {
  console.log('Resultados de búsqueda:', resultados);
})
.catch(error => {
  console.error('Error:', error);
});

En este ejemplo, se generará una URL como https://api.ejemplo.com/usuarios/buscar?nombre=Ana&ciudad=Madrid&edad_minima=25.

Envío de solicitudes POST y otros métodos

Para enviar datos al servidor, normalmente utilizamos el método POST. Otros métodos comunes incluyen PUT (para actualizar recursos), DELETE (para eliminar recursos) y PATCH (para actualizar parcialmente recursos).

Ejemplo de POST

// Función para crear un nuevo usuario
function crearUsuario(datosUsuario) {
  return fetch('https://api.ejemplo.com/usuarios', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(datosUsuario)
  })
  .then(respuesta => {
    if (!respuesta.ok) {
      // Si el servidor responde con un error, lo procesamos
      return respuesta.json().then(errorDatos => {
        throw new Error(errorDatos.mensaje || 'Error al crear usuario');
      });
    }
    return respuesta.json();
  });
}

// Ejemplo de uso
const nuevoUsuario = {
  nombre: 'Carlos Rodríguez',
  email: 'carlos@ejemplo.com',
  edad: 32,
  ciudad: 'Barcelona'
};

crearUsuario(nuevoUsuario)
  .then(usuarioCreado => {
    console.log('Usuario creado con éxito:', usuarioCreado);
  })
  .catch(error => {
    console.error('Error al crear usuario:', error);
  });

Ejemplo de PUT

// Función para actualizar un usuario existente
function actualizarUsuario(id, datosActualizados) {
  return fetch(`https://api.ejemplo.com/usuarios/${id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(datosActualizados)
  })
  .then(respuesta => {
    if (!respuesta.ok) throw new Error('Error al actualizar');
    return respuesta.json();
  });
}

// Ejemplo de uso
actualizarUsuario(123, {
  edad: 33,
  ciudad: 'Valencia'
})
.then(resultado => {
  console.log('Usuario actualizado:', resultado);
})
.catch(error => {
  console.error('Error:', error);
});

Ejemplo de DELETE

// Función para eliminar un usuario
function eliminarUsuario(id) {
  return fetch(`https://api.ejemplo.com/usuarios/${id}`, {
    method: 'DELETE'
  })
  .then(respuesta => {
    if (!respuesta.ok) throw new Error('Error al eliminar');
    
    // Algunos endpoints devuelven un cuerpo vacío en DELETE
    // En ese caso, simplemente retornamos true
    if (respuesta.status === 204) {
      return true;
    }
    
    return respuesta.json();
  });
}

// Ejemplo de uso
eliminarUsuario(123)
  .then(resultado => {
    console.log('Usuario eliminado correctamente');
  })
  .catch(error => {
    console.error('Error al eliminar:', error);
  });

Trabajo con headers y body

Las cabeceras HTTP (headers) y el cuerpo (body) de las peticiones y respuestas son componentes importantes en la comunicación con APIs.

Manipulación de headers

Las cabeceras permiten enviar metadatos adicionales con nuestras peticiones, como tokens de autorización, preferencias de formato o información del cliente:

// Configuración común de headers
const headers = new Headers({
  'Content-Type': 'application/json',
  'Accept-Language': 'es-ES',
  'X-API-Key': 'abc123xyz789'
});

// Añadir una cabecera individualmente
headers.append('User-Agent', 'MiAplicacionWeb/1.0');

// Verificar si existe una cabecera
if (headers.has('X-API-Key')) {
  console.log('La clave API está presente');
}

// Obtener el valor de una cabecera
const apiKey = headers.get('X-API-Key');

// Usar las cabeceras en una petición
fetch('https://api.ejemplo.com/datos', {
  headers: headers
})
.then(respuesta => respuesta.json())
.then(datos => console.log(datos));

Trabajando con el cuerpo de la respuesta

El objeto Response proporciona varios métodos para procesar el cuerpo de la respuesta en diferentes formatos:

fetch('https://api.ejemplo.com/datos')
  .then(respuesta => {
    // Información sobre la respuesta
    console.log('Status:', respuesta.status);
    console.log('OK:', respuesta.ok);
    console.log('Tipo de contenido:', respuesta.headers.get('Content-Type'));
    
    // Dependiendo del tipo de contenido, procesamos la respuesta
    if (respuesta.headers.get('Content-Type').includes('application/json')) {
      return respuesta.json(); // Datos JSON
    } else if (respuesta.headers.get('Content-Type').includes('text/')) {
      return respuesta.text(); // Contenido de texto
    } else if (respuesta.headers.get('Content-Type').includes('image/')) {
      return respuesta.blob(); // Datos binarios como imágenes
    } else {
      return respuesta.arrayBuffer(); // Datos binarios genéricos
    }
  })
  .then(datos => {
    console.log('Datos procesados:', datos);
  });

Los métodos más comunes para procesar el cuerpo de la respuesta son:

  • json(): Convierte el cuerpo a un objeto JavaScript (para respuestas en formato JSON)
  • text(): Devuelve el cuerpo como texto
  • blob(): Para datos binarios como imágenes
  • arrayBuffer(): Para datos binarios genéricos
  • formData(): Para datos en formato FormData

Es importante recordar que estos métodos solo pueden ser llamados una vez, ya que consumen el cuerpo de la respuesta.

Manejo de respuestas

Un aspecto fundamental al trabajar con Fetch es comprender cómo gestionar diferentes tipos de respuestas y estados HTTP:

fetch('https://api.ejemplo.com/recurso')
  .then(respuesta => {
    // Verificar el estado de la respuesta
    if (respuesta.status === 200) {
      console.log('Respuesta exitosa');
    } else if (respuesta.status === 404) {
      console.log('Recurso no encontrado');
      throw new Error('No se encontró el recurso solicitado');
    } else if (respuesta.status === 401) {
      console.log('No autorizado');
      throw new Error('Se requiere autenticación');
    } else if (respuesta.status >= 500) {
      console.log('Error del servidor');
      throw new Error('Error interno del servidor');
    }
    
    // Procesar la respuesta
    return respuesta.json();
  })
  .then(datos => {
    // Trabajar con los datos
    console.log('Datos:', datos);
  })
  .catch(error => {
    // Manejar cualquier error ocurrido
    console.error('Error en la operación:', error);
  });

Un patrón común es verificar la propiedad ok del objeto Response, que es true para códigos de estado en el rango 200-299:

fetch('https://api.ejemplo.com/datos')
  .then(respuesta => {
    if (!respuesta.ok) {
      throw new Error(`Error HTTP: ${respuesta.status}`);
    }
    return respuesta.json();
  })
  .then(datos => {
    console.log('Datos recibidos:', datos);
  })
  .catch(error => {
    console.error('Error:', error);
  });

Ventajas sobre XMLHttpRequest

La API Fetch ofrece varias ventajas significativas frente a XMLHttpRequest (XHR):

  1. Sintaxis más simple y limpia:

    • XHR requiere varios pasos y configuraciones para una petición simple
    • Fetch utiliza un enfoque más directo y legible
  2. Basada en Promesas:

    • XHR utiliza callbacks, que pueden llevar a "callback hell"
    • Fetch devuelve promesas, que facilitan el encadenamiento y el uso de async/await
  3. Mejor manejo de errores:

    • La gestión de errores es más intuitiva con Fetch gracias a .catch()
    • Se integra perfectamente con los bloques try/catch de async/await
  4. Request y Response más potentes:

    • Fetch proporciona objetos especializados para peticiones y respuestas
    • Ofrece métodos específicos para diferentes tipos de contenido (json, text, blob, etc.)
  5. Mejor soporte para streaming:

    • Fetch permite el procesamiento de datos en streaming, lo que es más eficiente para grandes volúmenes de datos

Integración con async/await

Una de las grandes ventajas de Fetch es su perfecta integración con async/await, lo que nos permite escribir código asíncrono que parece síncrono, mejorando considerablemente la legibilidad:

// Función asíncrona para obtener datos de múltiples recursos
async function obtenerDatosCompletos(idUsuario) {
  try {
    // Obtener información básica del usuario
    const respuestaUsuario = await fetch(`https://api.ejemplo.com/usuarios/${idUsuario}`);
    if (!respuestaUsuario.ok) throw new Error('Error al obtener usuario');
    const usuario = await respuestaUsuario.json();
    
    // Obtener publicaciones del usuario
    const respuestaPublicaciones = await fetch(`https://api.ejemplo.com/usuarios/${idUsuario}/publicaciones`);
    if (!respuestaPublicaciones.ok) throw new Error('Error al obtener publicaciones');
    const publicaciones = await respuestaPublicaciones.json();
    
    // Obtener comentarios del usuario
    const respuestaComentarios = await fetch(`https://api.ejemplo.com/usuarios/${idUsuario}/comentarios`);
    if (!respuestaComentarios.ok) throw new Error('Error al obtener comentarios');
    const comentarios = await respuestaComentarios.json();
    
    // Combinar todos los datos
    return {
      perfil: usuario,
      publicaciones: publicaciones,
      comentarios: comentarios
    };
  } catch (error) {
    console.error('Error obteniendo datos completos:', error);
    throw error;
  }
}

// Uso de la función
async function mostrarPerfilCompleto(idUsuario) {
  try {
    const datosCompletos = await obtenerDatosCompletos(idUsuario);
    
    console.log('Nombre:', datosCompletos.perfil.nombre);
    console.log('Total de publicaciones:', datosCompletos.publicaciones.length);
    console.log('Total de comentarios:', datosCompletos.comentarios.length);
    
    // Aquí podríamos actualizar la interfaz con estos datos
  } catch (error) {
    console.error('No se pudo mostrar el perfil:', error);
    // Mostrar mensaje de error al usuario
  }
}

// Llamar a la función con un ID de usuario
mostrarPerfilCompleto(123);

Este enfoque con async/await hace que el código sea mucho más fácil de leer, entender y mantener, especialmente cuando necesitamos realizar múltiples peticiones secuenciales o dependientes entre sí.

Resumen

La API Fetch representa una evolución significativa en la forma de realizar peticiones HTTP desde JavaScript. Su sintaxis clara, su enfoque basado en promesas y su perfecta integración con características modernas como async/await la convierten en la opción preferida para la comunicación cliente-servidor en aplicaciones web actuales.

A lo largo de este artículo, hemos explorado cómo utilizar Fetch para realizar diferentes tipos de peticiones, gestionar respuestas y aprovechar sus capacidades para construir aplicaciones robustas. Aunque hemos cubierto los aspectos más importantes, Fetch ofrece muchas más posibilidades que se pueden explorar a medida que profundices en tu desarrollo con JavaScript.

En el próximo artículo, exploraremos con más detalle los métodos HTTP y cómo se utilizan en el contexto de las APIs RESTful, un conocimiento fundamental para cualquier desarrollador web moderno.