Fetch API y AJAX
Aprende a realizar peticiones HTTP desde JavaScript para comunicarte con servidores y APIs externas utilizando la Fetch API y técnicas AJAX.
Cristian Escalante
Última actualización: 21 de abril de 2025
Introducción a AJAX y peticiones HTTP
AJAX (Asynchronous JavaScript And XML) es una técnica que permite actualizar partes de una página web sin necesidad de recargarla completamente. Aunque su nombre incluye XML, actualmente se utiliza principalmente con JSON.
¿Qué es AJAX?
AJAX combina:
- JavaScript asíncrono
- El objeto XMLHttpRequest (o la moderna Fetch API)
- DOM para mostrar/manipular información
- JSON o XML para intercambiar datos
Ventajas de AJAX
- Mejora la experiencia del usuario
- Reduce el tráfico de red al cargar solo los datos necesarios
- Disminuye la carga del servidor
- Permite actualizaciones parciales de la página
El protocolo HTTP
HTTP (Hypertext Transfer Protocol) es el protocolo de comunicación que permite las transferencias de información en la web.
Componentes de una petición HTTP
- Método HTTP: GET, POST, PUT, DELETE, etc.
- URL: Dirección del recurso
- Headers: Metadatos de la petición
- Body: Datos enviados (en POST, PUT, etc.)
Métodos HTTP comunes
- GET: Solicitar datos
- POST: Enviar datos para crear un recurso
- PUT: Actualizar un recurso existente
- DELETE: Eliminar un recurso
- PATCH: Actualizar parcialmente un recurso
- OPTIONS: Obtener métodos HTTP permitidos
Códigos de estado HTTP
- 1xx: Informativo
- 2xx: Éxito (200 OK, 201 Created, 204 No Content)
- 3xx: Redirección (301 Moved Permanently, 304 Not Modified)
- 4xx: Error del cliente (400 Bad Request, 401 Unauthorized, 404 Not Found)
- 5xx: Error del servidor (500 Internal Server Error, 503 Service Unavailable)
La Fetch API
La Fetch API es una interfaz moderna para realizar peticiones HTTP en JavaScript, basada en Promesas.
Sintaxis básica
fetch(url)
.then(response => {
// Manejar la respuesta
return response.json(); // Convertir a JSON
})
.then(data => {
// Trabajar con los datos
console.log(data);
})
.catch(error => {
// Manejar errores
console.error('Error:', error);
});
Con async/await
async function obtenerDatos() {
try {
const response = await fetch(url);
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
Verificar si la petición fue exitosa
Es importante verificar el estado de la respuesta:
fetch('https://api.ejemplo.com/datos')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Métodos HTTP con Fetch
GET (obtener datos)
// Petición GET básica
fetch('https://api.ejemplo.com/usuarios')
.then(response => response.json())
.then(data => console.log(data));
// GET con parámetros de consulta
fetch('https://api.ejemplo.com/usuarios?rol=admin&activo=true')
.then(response => response.json())
.then(data => console.log(data));
// Alternativa para construir URL con parámetros
const params = new URLSearchParams({
rol: 'admin',
activo: true
});
fetch(`https://api.ejemplo.com/usuarios?${params}`)
.then(response => response.json())
.then(data => console.log(data));
POST (enviar datos)
const nuevoUsuario = {
nombre: 'Ana García',
email: 'ana@ejemplo.com',
rol: 'editor'
};
fetch('https://api.ejemplo.com/usuarios', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(nuevoUsuario)
})
.then(response => response.json())
.then(data => console.log('Usuario creado:', data))
.catch(error => console.error('Error:', error));
PUT (actualizar datos)
const usuarioActualizado = {
nombre: 'Ana García Martínez',
email: 'ana.garcia@ejemplo.com',
rol: 'admin'
};
fetch('https://api.ejemplo.com/usuarios/123', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(usuarioActualizado)
})
.then(response => response.json())
.then(data => console.log('Usuario actualizado:', data))
.catch(error => console.error('Error:', error));
DELETE (eliminar datos)
fetch('https://api.ejemplo.com/usuarios/123', {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error('Error al eliminar usuario');
})
.then(data => console.log('Usuario eliminado:', data))
.catch(error => console.error('Error:', error));
PATCH (actualización parcial)
const cambios = {
rol: 'admin'
};
fetch('https://api.ejemplo.com/usuarios/123', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(cambios)
})
.then(response => response.json())
.then(data => console.log('Usuario parcialmente actualizado:', data))
.catch(error => console.error('Error:', error));
Manejo de respuestas y errores
Verificación completa de respuestas
fetch('https://api.ejemplo.com/datos')
.then(response => {
// Verificar el estado HTTP
if (!response.ok) {
// Crear un error con el estado
throw new Error(`Error HTTP: ${response.status} ${response.statusText}`);
}
// Verificar el tipo de contenido
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new TypeError('La respuesta no es JSON');
}
return response.json();
})
.then(data => {
console.log('Datos recibidos:', data);
})
.catch(error => {
console.error('Error en la petición:', error.message);
});
Timeout para peticiones
// Función para abortar fetch después de un tiempo límite
function fetchConTimeout(url, options, timeout = 5000) {
// Crear un controlador de aborto
const controller = new AbortController();
const { signal } = controller;
// Configurar el temporizador para abortar
const timeoutId = setTimeout(() => controller.abort(), timeout);
// Realizar la petición con el signal
return fetch(url, { ...options, signal })
.then(response => {
clearTimeout(timeoutId);
return response;
})
.catch(error => {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`La petición excedió el tiempo límite de ${timeout}ms`);
}
throw error;
});
}
// Uso
fetchConTimeout('https://api.ejemplo.com/datos', {}, 3000)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error.message));
Manejo de errores de red
fetch('https://api-que-no-existe.com/datos')
.then(response => response.json())
.catch(error => {
if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
console.error('Error de red: No se pudo conectar al servidor');
// Mostrar mensaje amigable al usuario
mostrarMensajeError('No hay conexión a Internet o el servidor no está disponible');
} else {
console.error('Error desconocido:', error);
}
});
Trabajando con JSON
JSON (JavaScript Object Notation) es el formato estándar para intercambiar datos en aplicaciones web.
Convertir respuestas a JSON
fetch('https://api.ejemplo.com/datos')
.then(response => response.json()) // Convierte la respuesta a JSON
.then(data => console.log(data));
Otros formatos de respuesta
// Texto plano
fetch('https://api.ejemplo.com/texto')
.then(response => response.text())
.then(texto => console.log(texto));
// Blob (para imágenes, archivos, etc.)
fetch('https://api.ejemplo.com/imagen.jpg')
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);
});
// ArrayBuffer (para datos binarios)
fetch('https://api.ejemplo.com/datos-binarios')
.then(response => response.arrayBuffer())
.then(buffer => {
// Procesar el buffer
});
// FormData
fetch('https://api.ejemplo.com/formulario')
.then(response => response.formData())
.then(formData => {
// Acceder a los datos del formulario
console.log(formData.get('campo'));
});
Enviar diferentes tipos de datos
// Enviar JSON
fetch('https://api.ejemplo.com/datos', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ nombre: 'Ana', edad: 28 })
});
// Enviar FormData (para formularios)
const formData = new FormData();
formData.append('nombre', 'Ana');
formData.append('archivo', fileInput.files[0]);
fetch('https://api.ejemplo.com/subir', {
method: 'POST',
body: formData
// No es necesario establecer Content-Type, se configura automáticamente
});
// Enviar texto plano
fetch('https://api.ejemplo.com/texto', {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: 'Texto simple para enviar'
});
Headers y opciones de configuración
Configurar headers
fetch('https://api.ejemplo.com/datos', {
headers: {
'Authorization': 'Bearer token123',
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Custom-Header': 'valor personalizado'
}
})
.then(response => response.json())
.then(data => console.log(data));
Leer headers de respuesta
fetch('https://api.ejemplo.com/datos')
.then(response => {
// Obtener un header específico
console.log('Content-Type:', response.headers.get('content-type'));
console.log('X-Rate-Limit:', response.headers.get('x-rate-limit'));
// Iterar sobre todos los headers
response.headers.forEach((valor, nombre) => {
console.log(`${nombre}: ${valor}`);
});
return response.json();
})
.then(data => console.log(data));
Opciones de configuración de Fetch
fetch('https://api.ejemplo.com/datos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clave: 'valor' }),
mode: 'cors', // 'cors', 'no-cors', 'same-origin'
credentials: 'include', // 'omit', 'same-origin', 'include'
cache: 'no-cache', // 'default', 'no-store', 'reload', 'no-cache', 'force-cache', 'only-if-cached'
redirect: 'follow', // 'follow', 'error', 'manual'
referrerPolicy: 'no-referrer', // 'no-referrer', 'no-referrer-when-downgrade', 'origin', etc.
signal: controller.signal // Para abortar la petición
})
.then(response => response.json())
.then(data => console.log(data));
Autenticación
// Autenticación básica
const credenciales = btoa('usuario:contraseña'); // Codificar en Base64
fetch('https://api.ejemplo.com/datos-protegidos', {
headers: {
'Authorization': `Basic ${credenciales}`
}
})
.then(response => response.json())
.then(data => console.log(data));
// Autenticación con token JWT
const token = obtenerTokenDelAlmacenamiento();
fetch('https://api.ejemplo.com/datos-protegidos', {
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => response.json())
.then(data => console.log(data));
Cross-Origin Resource Sharing (CORS)
CORS es un mecanismo de seguridad que permite o restringe las peticiones HTTP de origen cruzado (desde un dominio a otro).
¿Qué es CORS?
- Mecanismo de seguridad implementado por los navegadores
- Controla qué dominios pueden acceder a recursos en otro dominio
- Evita peticiones no autorizadas desde scripts maliciosos
Tipos de peticiones CORS
- Peticiones simples: No activan una petición de preflight
- Métodos: GET, HEAD, POST
- Headers: solo los permitidos (Accept, Content-Type, etc.)
- Content-Type: application/x-www-form-urlencoded, multipart/form-data, o text/plain
- Peticiones no simples: Activan una petición de preflight (OPTIONS)
- Otros métodos: PUT, DELETE, etc.
- Headers personalizados
- Content-Type: application/json, etc.
Manejo de errores CORS
fetch('https://api-otro-dominio.com/datos')
.catch(error => {
if (error instanceof TypeError && error.message.includes('CORS')) {
console.error('Error de CORS: El servidor no permite peticiones desde este origen');
// Mostrar mensaje al usuario
mostrarError('No se puede acceder a este recurso debido a restricciones de seguridad');
} else {
console.error('Error:', error);
}
});
Soluciones a problemas de CORS
- Configuración del servidor: El servidor debe enviar los headers CORS adecuados
Access-Control-Allow-Origin: https://tudominio.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, Authorization
- Proxy del servidor: Utilizar un servidor propio como intermediario
// En lugar de llamar directamente a la API externa fetch('/api/proxy?url=https://api-externa.com/datos') .then(response => response.json()) .then(data => console.log(data));
- JSONP: Técnica antigua para peticiones GET (no recomendada para nuevos desarrollos)
- Modo no-cors: Limita lo que puedes hacer con la respuesta
fetch('https://api-otro-dominio.com/datos', { mode: 'no-cors' }) .then(response => { // La respuesta es de tipo "opaque" y no se puede leer el contenido console.log(response.type); // "opaque" });
Alternativas a Fetch
Axios
Axios es una biblioteca popular para realizar peticiones HTTP:
// Instalación: npm install axios
// Importar
import axios from 'axios';
// Petición GET
axios.get('https://api.ejemplo.com/usuarios')
.then(response => {
console.log(response.data); // Los datos ya vienen como JSON
})
.catch(error => {
console.error('Error:', error);
});
// Petición POST
axios.post('https://api.ejemplo.com/usuarios', {
nombre: 'Ana',
email: 'ana@ejemplo.com'
})
.then(response => console.log(response.data))
.catch(error => console.error('Error:', error));
// Con async/await
async function obtenerDatos() {
try {
const response = await axios.get('https://api.ejemplo.com/usuarios');
return response.data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
Ventajas de Axios sobre Fetch
- Transformación automática de datos JSON
- Interceptores de peticiones y respuestas
- Cancelación de peticiones integrada
- Timeouts integrados
- Protección XSRF
- Seguimiento de progreso en subida/descarga
- Compatibilidad con navegadores más antiguos
// Configuración global
axios.defaults.baseURL = 'https://api.ejemplo.com';
axios.defaults.headers.common['Authorization'] = 'Bearer token123';
// Crear instancia con configuración personalizada
const api = axios.create({
baseURL: 'https://api.ejemplo.com/v2',
timeout: 5000,
headers: {'X-Custom-Header': 'valor'}
});
// Interceptores
axios.interceptors.request.use(
config => {
// Modificar la petición antes de enviarla
config.headers.Authorization = `Bearer ${getToken()}`;
return config;
},
error => {
// Manejar error de petición
return Promise.reject(error);
}
);
axios.interceptors.response.use(
response => {
// Cualquier código de estado entre 2xx hace que esta función se active
return response;
},
error => {
// Cualquier código de estado fuera del rango 2xx hace que esta función se active
if (error.response.status === 401) {
// Redirigir al login
window.location = '/login';
}
return Promise.reject(error);
}
);
XMLHttpRequest (método tradicional)
Antes de Fetch, se usaba XMLHttpRequest:
function realizarPeticion(url, metodo, datos, callback) {
const xhr = new XMLHttpRequest();
xhr.open(metodo, url, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
const respuesta = JSON.parse(xhr.responseText);
callback(null, respuesta);
} else {
callback(new Error(`Error HTTP: ${xhr.status}`));
}
};
xhr.onerror = function() {
callback(new Error('Error de red'));
};
if (datos) {
xhr.send(JSON.stringify(datos));
} else {
xhr.send();
}
}
// Uso
realizarPeticion(
'https://api.ejemplo.com/usuarios',
'GET',
null,
function(error, datos) {
if (error) {
console.error('Error:', error);
return;
}
console.log('Datos:', datos);
}
);
Ejemplos prácticos
Aplicación de lista de tareas con API REST
// URL de la API
const API_URL = 'https://api.ejemplo.com/tareas';
// Obtener todas las tareas
async function obtenerTareas() {
try {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`);
}
const tareas = await response.json();
mostrarTareas(tareas);
return tareas;
} catch (error) {
console.error('Error al obtener tareas:', error);
mostrarError('No se pudieron cargar las tareas');
}
}
// Crear una nueva tarea
async function crearTarea(tarea) {
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(tarea)
});
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`);
}
const nuevaTarea = await response.json();
mostrarMensaje('Tarea creada con éxito');
return nuevaTarea;
} catch (error) {
console.error('Error al crear tarea:', error);
mostrarError('No se pudo crear la tarea');
}
}
// Actualizar estado de una tarea
async function actualizarEstadoTarea(id, completada) {
try {
const response = await fetch(`${API_URL}/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ completada })
});
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Error al actualizar tarea ${id}:`, error);
mostrarError('No se pudo actualizar la tarea');
}
}
// Eliminar una tarea
async function eliminarTarea(id) {
try {
const response = await fetch(`${API_URL}/${id}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`);
}
mostrarMensaje('Tarea eliminada con éxito');
return true;
} catch (error) {
console.error(`Error al eliminar tarea ${id}:`, error);
mostrarError('No se pudo eliminar la tarea');
return false;
}
}
// Ejemplo de uso
document.addEventListener('DOMContentLoaded', () => {
// Cargar tareas al iniciar
obtenerTareas();
// Manejar envío del formulario para crear tarea
document.getElementById('formularioTarea').addEventListener('submit', async (e) => {
e.preventDefault();
const titulo = document.getElementById('tituloTarea').value;
if (!titulo.trim()) return;
const nuevaTarea = await crearTarea({ titulo, completada: false });
if (nuevaTarea) {
document.getElementById('formularioTarea').reset();
obtenerTareas(); // Recargar lista
}
});
});
Consumir una API pública
// Ejemplo con la API de GitHub
async function buscarUsuariosGitHub(consulta) {
try {
const response = await fetch(`https://api.github.com/search/users?q=${encodeURIComponent(consulta)}`);
if (!response.ok) {
if (response.status === 403) {
throw new Error('Límite de peticiones excedido. Intenta más tarde.');
}
throw new Error(`Error HTTP: ${response.status}`);
}
const data = await response.json();
return data.items;
} catch (error) {
console.error('Error al buscar usuarios:', error);
throw error;
}
}
// Uso
buscarUsuariosGitHub('javascript')
.then(usuarios => {
console.log('Usuarios encontrados:', usuarios);
// Mostrar resultados en la UI
usuarios.forEach(usuario => {
console.log(`- ${usuario.login} (${usuario.html_url})`);
});
})
.catch(error => {
console.error('Error:', error.message);
});
Mejores prácticas
- Manejar siempre los errores:
fetch(url) .then(response => { if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); return response.json(); }) .catch(error => console.error('Error:', error));
- Usar async/await para código más limpio:
async function obtenerDatos() { try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); return await response.json(); } catch (error) { console.error('Error:', error); throw error; } }
- Implementar timeouts para evitar peticiones que nunca terminan:
const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); fetch(url, { signal: controller.signal }) .then(response => { clearTimeout(timeoutId); return response.json(); }) .catch(error => { clearTimeout(timeoutId); console.error('Error:', error); });
- Centralizar la lógica de peticiones:
// Crear un servicio o cliente HTTP reutilizable const apiClient = { async get(endpoint) { return this.request(endpoint, 'GET'); }, async post(endpoint, data) { return this.request(endpoint, 'POST', data); }, async request(endpoint, method, data = null) { const options = { method, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.getToken()}` } }; if (data) { options.body = JSON.stringify(data); } const response = await fetch(`${this.baseURL}${endpoint}`, options); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } return response.json(); }, baseURL: 'https://api.ejemplo.com', getToken() { return localStorage.getItem('token'); } }; // Uso apiClient.get('/usuarios') .then(usuarios => console.log(usuarios)) .catch(error => console.error(error));
- Mostrar estados de carga y errores al usuario:
async function cargarDatos() { const contenedor = document.querySelector('.datos-container'); try { // Mostrar indicador de carga contenedor.innerHTML = '<div class="cargando">Cargando...</div>'; // Realizar petición const datos = await obtenerDatos(); // Mostrar datos if (datos.length === 0) { contenedor.innerHTML = '<div class="sin-resultados">No hay resultados</div>'; } else { contenedor.innerHTML = datos.map(item => `<div class="item">${item.nombre}</div>`).join(''); } } catch (error) { // Mostrar error contenedor.innerHTML = `<div class="error">Error: ${error.message}</div>`; } }