fetch API en JavaScript: peticiones HTTP modernas sin XMLHttpRequest

La Fetch API es la forma moderna de hacer peticiones HTTP en JavaScript, reemplazando a XMLHttpRequest con una interfaz basada en Promises mucho más limpia. Está disponible de forma nativa en todos los navegadores modernos y en Node.js desde la versión 18, y funciona perfectamente con async/await.

GET básico: leer datos de una API

fetch(url) devuelve una Promise que resuelve a un objeto Response. Hay que llamar a response.json() (u otros métodos) para leer el cuerpo, ya que también devuelve una Promise:

async function obtenerUsuario(id) {
  const response = await fetch(`https://api.ejemplo.com/usuarios/${id}`);

  // El error más frecuente: fetch NO lanza error con status 4xx/5xx
  // response.ok es false si status >= 400
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  const usuario = await response.json();
  return usuario;
}

try {
  const usuario = await obtenerUsuario(1);
  console.log(usuario.nombre);
} catch (err) {
  // Aquí llegan tanto errores de red como los throw manuales
  console.error('Error:', err.message);
}

// Información de la respuesta:
// response.status     ? 200, 404, 500...
// response.ok         ? true si status 200-299
// response.headers    ? objeto Headers
// response.url        ? URL final (tras redirecciones)

POST: enviar datos

Para POST, hay que especificar el método, el tipo de contenido y el cuerpo serializado:

async function crearUsuario(datos) {
  const response = await fetch('/api/usuarios', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify(datos)
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.mensaje || `HTTP ${response.status}`);
  }

  return response.json();
}

const nuevoUsuario = await crearUsuario({
  nombre: 'Ana',
  email: '[email protected]',
  rol: 'editor'
});
console.log('Creado con ID:', nuevoUsuario.id);

// Enviar FormData (para uploads de archivos):
async function subirArchivo(archivo) {
  const formData = new FormData();
  formData.append('archivo', archivo);
  formData.append('nombre', archivo.name);

  // Con FormData, NO pongas Content-Type: el navegador lo establece
  // automáticamente con el boundary correcto
  const response = await fetch('/api/upload', {
    method: 'POST',
    body: formData
  });
  return response.json();
}

Leer diferentes tipos de respuesta

Response ofrece varios métodos para leer el cuerpo según el formato:

const r = await fetch('/api/datos');

// Solo se puede leer el body UNA VEZ
await r.json();    // Parsea como JSON
await r.text();    // Texto plano
await r.blob();    // Archivo binario (imagen, PDF...)
await r.arrayBuffer(); // Buffer de bytes
await r.formData();    // FormData

// Para imagen:
async function obtenerImagen(url) {
  const r = await fetch(url);
  const blob = await r.blob();
  const urlObjeto = URL.createObjectURL(blob);
  document.querySelector('img').src = urlObjeto;
}

// Verificar el Content-Type antes de parsear:
async function obtenerDatos(url) {
  const r = await fetch(url);
  const tipo = r.headers.get('content-type') ?? '';

  if (tipo.includes('application/json')) {
    return r.json();
  } else if (tipo.includes('text/')) {
    return r.text();
  } else {
    return r.blob();
  }
}

Cancelar peticiones con AbortController

Si el usuario navega a otra pantalla antes de que llegue la respuesta, o si quieres implementar un timeout, necesitas cancelar la petición:

// Cancelación manual
const controller = new AbortController();
const { signal } = controller;

const promesa = fetch('/api/datos', { signal });

// Cancelar tras 3 segundos:
const timeout = setTimeout(() => controller.abort(), 3000);

try {
  const datos = await promesa;
  clearTimeout(timeout);
  return datos.json();
} catch (err) {
  if (err.name === 'AbortError') {
    console.log('Petición cancelada');
  } else {
    throw err;
  }
}

// En React: cancelar al desmontar el componente
useEffect(() => {
  const controller = new AbortController();

  fetch('/api/datos', { signal: controller.signal })
    .then(r => r.json())
    .then(setDatos)
    .catch(err => {
      if (err.name !== 'AbortError') console.error(err);
    });

  return () => controller.abort();  // Limpieza al desmontar
}, []);

fetch vs axios: cuándo elegir cada uno

// Ventajas de fetch (nativo):
// - Sin dependencias
// - Nativo en navegadores y Node 18+
// - API más cercana al estándar HTTP

// Ventajas de axios sobre fetch:
// - Lanza error automáticamente con status >= 400 (fetch no lo hace)
// - Transforma automáticamente JSON en request y response
// - Interceptores de request/response
// - Cancelación con CancelToken (aunque fetch+AbortController es similar)
// - Soporte de progreso de upload

// Wrapper básico de fetch que se comporta como axios:
async function api(url, opciones = {}) {
  const { data, ...resto } = opciones;
  const config = {
    ...resto,
    headers: {
      'Content-Type': 'application/json',
      ...resto.headers
    },
    body: data ? JSON.stringify(data) : undefined
  };

  const r = await fetch(url, config);
  if (!r.ok) {
    const err = new Error(`HTTP ${r.status}`);
    err.response = r;
    throw err;
  }
  return r.json();
}

El punto más importante para recordar: fetch solo lanza errores en caso de fallo de red o de cancelación. Un status 404 o 500 desde el servidor no es un error para fetch: la Promise se resuelve normalmente. Siempre comprueba response.ok o el status code.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP