Fetch y APIs REST

Cómo comunicarse con servidores usando la Fetch API: GET, POST, manejo de errores y AbortController.

La Fetch API es la forma nativa de hacer peticiones HTTP desde el navegador. Reemplaza al antiguo XMLHttpRequest con una interfaz limpia basada en promesas.


GET básico

fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then((res) => res.json())
  .then((data) => console.log(data));

// Con async/await
async function obtenerPost(id) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  const data = await res.json();
  return data;
}

El objeto Response

fetch() devuelve una promesa con el objeto Response. Este objeto no es directamente el cuerpo — hay que leerlo con un método apropiado:

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

res.ok          // true si status 200–299
res.status      // número HTTP: 200, 404, 500...
res.statusText  // 'OK', 'Not Found'...
res.headers     // objeto Headers con las cabeceras de respuesta
res.url         // URL final (después de posibles redirects)

// Métodos para leer el cuerpo (solo uno, no se puede leer dos veces)
await res.json()      // JSON → objeto JavaScript
await res.text()      // texto plano, HTML, CSV...
await res.blob()      // datos binarios (imágenes, archivos)
await res.formData()  // FormData

Manejo correcto de errores

Atención: fetch() no lanza un error en respuestas 4xx o 5xx. Solo rechaza la promesa si hay un fallo de red (sin conexión, DNS inexistente…). Debes comprobar res.ok manualmente:

async function obtenerDatos(url) {
  try {
    const res = await fetch(url);

    if (!res.ok) {
      throw new Error(`Error HTTP: ${res.status}`);
    }

    return await res.json();

  } catch (error) {
    // Aquí llegan tanto errores de red como el throw de arriba
    console.error('Fallo en la petición:', error.message);
    throw error;  // re-lanzar para que el llamador lo maneje
  }
}

POST con cuerpo JSON

async function crearPost(datos) {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(datos),
  });

  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

const nuevo = await crearPost({
  title: 'Mi post',
  body: 'Contenido del post',
  userId: 1,
});
console.log(nuevo); // { id: 101, title: 'Mi post', ... }

Métodos HTTP habituales

// GET — obtener datos
fetch('/api/posts')

// POST — crear un recurso
fetch('/api/posts', { method: 'POST', headers: {...}, body: JSON.stringify(datos) })

// PUT — reemplazar un recurso completo
fetch('/api/posts/1', { method: 'PUT', headers: {...}, body: JSON.stringify(datos) })

// PATCH — actualizar campos concretos
fetch('/api/posts/1', { method: 'PATCH', headers: {...}, body: JSON.stringify({ title: 'Nuevo título' }) })

// DELETE — eliminar
fetch('/api/posts/1', { method: 'DELETE' })

Cabeceras de autenticación

const token = localStorage.getItem('token');

const res = await fetch('/api/perfil', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  }
});

Enviar formularios

// FormData encapsula los campos del formulario
const form = document.querySelector('#mi-formulario');

form.addEventListener('submit', async (e) => {
  e.preventDefault();

  const formData = new FormData(form);

  // Enviar como multipart/form-data (necesario para archivos)
  const res = await fetch('/api/subir', {
    method: 'POST',
    body: formData,  // NO pongas Content-Type — el navegador lo hace solo con el boundary
  });

  const resultado = await res.json();
  console.log(resultado);
});

AbortController — cancelar peticiones

const controller = new AbortController();

// Cancelar si pasan más de 5 segundos
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  const res = await fetch('/api/datos-lentos', {
    signal: controller.signal,  // vincula el fetch al controller
  });
  clearTimeout(timeout);
  return await res.json();

} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Petición cancelada');
  } else {
    throw error;
  }
}

Cancelar también es útil en React/Astro cuando el componente se desmonta antes de que termine la petición.


Patrón: función fetch reutilizable

async function api(endpoint, opciones = {}) {
  const base = 'https://api.ejemplo.com';
  const token = localStorage.getItem('token');

  const res = await fetch(`${base}${endpoint}`, {
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...opciones.headers,
    },
    ...opciones,
  });

  if (!res.ok) {
    const cuerpo = await res.json().catch(() => ({}));
    throw new Error(cuerpo.message || `HTTP ${res.status}`);
  }

  // 204 No Content — respuesta vacía
  if (res.status === 204) return null;

  return res.json();
}

// Uso
const posts = await api('/posts');
const nuevo = await api('/posts', {
  method: 'POST',
  body: JSON.stringify({ title: 'Test' }),
});

Conceptos REST básicos

Una API REST organiza los recursos como URLs y usa los métodos HTTP para las operaciones:

GET    /api/posts        → lista de posts
GET    /api/posts/1      → un post concreto
POST   /api/posts        → crear un post
PUT    /api/posts/1      → reemplazar el post 1
PATCH  /api/posts/1      → actualizar campos del post 1
DELETE /api/posts/1      → eliminar el post 1

Los códigos de estado más comunes:

200 OK              — éxito general
201 Created         — recurso creado (respuesta a POST)
204 No Content      — éxito sin cuerpo (respuesta a DELETE)
400 Bad Request     — los datos enviados son incorrectos
401 Unauthorized    — no autenticado
403 Forbidden       — autenticado pero sin permiso
404 Not Found       — el recurso no existe
422 Unprocessable   — datos válidos pero fallan validación de negocio
500 Internal Server Error — error en el servidor

En la siguiente lección aprendemos a organizar el código en módulos: import, export, y cómo el sistema de módulos de JavaScript hace el código más mantenible.