Fetch API es la forma moderna de hacer peticiones HTTP en JavaScript. Sustituyó a XMLHttpRequest y es compatible con todos los navegadores modernos y con Node.js. Si sabes usarla bien, puedes consumir cualquier API REST de forma limpia y eficiente.
En esta guía vamos a ver todo lo que necesitas saber: desde la sintaxis básica hasta el manejo de errores, autenticación con tokens y patrones avanzados para proyectos reales.
En este artículo:
- ¿Qué es Fetch API?
- Tu primera petición GET
- Enviar datos con POST
- Manejo de errores correcto
- Headers y autenticación con JWT
- Async/await: la forma más limpia
- Patrones avanzados para proyectos reales
¿Qué es Fetch API?
Fetch API es una interfaz nativa del navegador (y de Node.js desde la versión 18) para hacer peticiones HTTP. Devuelve Promesas, lo que la hace perfectamente compatible con async/await.
A diferencia de XMLHttpRequest, Fetch tiene una sintaxis más limpia, es más fácil de usar y es el estándar actual para peticiones HTTP en JavaScript moderno.
Tu primera petición GET
La forma más básica de usar Fetch es pasarle una URL. Devuelve una Promesa que resuelve con un objeto Response:
// Petición GET básica
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json()) // Parsea el JSON
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// El mismo ejemplo con async/await (más limpio)
async function obtenerPost() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const data = await response.json();
console.log(data);
}
Importante: Fetch no rechaza la promesa cuando el servidor devuelve un error HTTP (404, 500…). Solo la rechaza si hay un error de red. Tienes que verificar response.ok manualmente.
Enviar datos con POST
Para enviar datos al servidor, usa el segundo argumento de fetch con las opciones de la petición:
async function crearPost(titulo, cuerpo) {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: titulo,
body: cuerpo,
userId: 1
})
});
if (!response.ok) {
throw new Error('Error al crear el post: ' + response.status);
}
const nuevoPost = await response.json();
console.log('Post creado:', nuevoPost);
return nuevoPost;
}
// Lo mismo funciona para PUT y PATCH (actualizar) y DELETE (eliminar)
async function eliminarPost(id) {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/' + id, {
method: 'DELETE'
});
return response.ok;
}
Manejo de errores correcto
El mayor error que comete la gente con Fetch es no manejar los errores HTTP. Como mencioné, Fetch solo rechaza la promesa con errores de red (sin conexión, DNS fallido…). Los errores del servidor (404, 401, 500…) hay que detectarlos manualmente:
async function fetchConErrores(url) {
let response;
try {
response = await fetch(url);
} catch (error) {
// Error de red: sin conexión, timeout, CORS, etc.
throw new Error('Error de red: ' + error.message);
}
// Error HTTP: el servidor respondió pero con un código de error
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error('Error HTTP ' + response.status + ': ' + (errorData.message || response.statusText));
}
return response.json();
}
// Uso
fetchConErrores('https://api.ejemplo.com/datos')
.then(data => console.log(data))
.catch(error => {
// Aquí manejas TODOS los errores posibles
console.error(error.message);
mostrarMensajeError(error.message);
});
Headers y autenticación con JWT
La mayoría de APIs protegidas usan autenticación con tokens JWT. El token se envía en el header Authorization:
// Petición autenticada con JWT
async function fetchAutenticado(url, opciones = {}) {
const token = localStorage.getItem('token');
const response = await fetch(url, {
...opciones,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
...opciones.headers // permite añadir headers extra
}
});
if (response.status === 401) {
// Token expirado o inválido — redirigir al login
localStorage.removeItem('token');
window.location.href = '/login';
return;
}
if (!response.ok) {
throw new Error('Error HTTP ' + response.status);
}
return response.json();
}
// Uso: igual que fetch pero con autenticación automática
const perfil = await fetchAutenticado('https://api.ejemplo.com/perfil');
const posts = await fetchAutenticado('https://api.ejemplo.com/posts');
Async/await: la forma más limpia
Aunque Fetch devuelve Promesas y puedes encadenar .then(), la forma más legible es async/await. Aquí un ejemplo completo de un módulo de API típico:
// api.js — módulo de API reutilizable
const BASE_URL = 'https://api.ejemplo.com';
function getToken() {
return localStorage.getItem('token');
}
async function request(endpoint, options = {}) {
const url = BASE_URL + endpoint;
const token = getToken();
const config = {
headers: {
'Content-Type': 'application/json',
...(token && { 'Authorization': 'Bearer ' + token }),
...options.headers
},
...options
};
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(error.message || 'Error ' + response.status);
}
// Para respuestas sin cuerpo (204 No Content)
if (response.status === 204) return null;
return response.json();
}
// Exportar métodos concretos para cada tipo de petición
export const api = {
get: (endpoint) => request(endpoint),
post: (endpoint, data) => request(endpoint, { method: 'POST', body: JSON.stringify(data) }),
put: (endpoint, data) => request(endpoint, { method: 'PUT', body: JSON.stringify(data) }),
delete: (endpoint) => request(endpoint, { method: 'DELETE' })
};
// Uso en cualquier parte de la app
import { api } from './api.js';
const usuarios = await api.get('/usuarios');
const nuevo = await api.post('/usuarios', { nombre: 'Ana', email: 'ana@ejemplo.com' });
await api.delete('/usuarios/5');
Patrones avanzados para proyectos reales
Cancelar peticiones con AbortController: útil cuando el usuario navega a otra página antes de que la petición termine, o cuando quieres implementar búsqueda en tiempo real con debounce:
// Cancelar peticiones anteriores en una búsqueda
let controladorAnterior = null;
async function buscar(query) {
// Cancelar la petición anterior si existe
if (controladorAnterior) {
controladorAnterior.abort();
}
controladorAnterior = new AbortController();
try {
const response = await fetch('/api/buscar?q=' + query, {
signal: controladorAnterior.signal
});
const resultados = await response.json();
mostrarResultados(resultados);
} catch (error) {
if (error.name === 'AbortError') {
// Petición cancelada intencionalmente — no hacer nada
return;
}
console.error('Error en búsqueda:', error);
}
}
Peticiones en paralelo: cuando necesitas varios datos independientes, lánzalos todos a la vez en lugar de esperar uno por uno:
// MAL: espera secuencial (lento)
const usuario = await api.get('/usuario/1'); // espera...
const posts = await api.get('/usuario/1/posts'); // espera otra vez...
const seguidores = await api.get('/usuario/1/seguidores'); // y otra vez...
// BIEN: peticiones en paralelo (rápido)
const [usuario, posts, seguidores] = await Promise.all([
api.get('/usuario/1'),
api.get('/usuario/1/posts'),
api.get('/usuario/1/seguidores')
]);
// Los tres se lanzan a la vez y se espera al más lento
Fetch API junto con async/await es todo lo que necesitas para trabajar con APIs REST en JavaScript moderno. Sin librerías externas, sin configuraciones complicadas. Una vez que tienes un módulo de API bien estructurado, añadir nuevos endpoints es cuestión de segundos.