HDP115

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.

CE

Cristian Escalante

Última actualización: 21 de abril de 2025

javascript
programación web
desarrollo frontend

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

  1. Método HTTP: GET, POST, PUT, DELETE, etc.
  2. URL: Dirección del recurso
  3. Headers: Metadatos de la petición
  4. 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

  1. 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
  2. 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

  1. 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
    
  2. 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));
    
  3. JSONP: Técnica antigua para peticiones GET (no recomendada para nuevos desarrollos)
  4. 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

  1. Transformación automática de datos JSON
  2. Interceptores de peticiones y respuestas
  3. Cancelación de peticiones integrada
  4. Timeouts integrados
  5. Protección XSRF
  6. Seguimiento de progreso en subida/descarga
  7. 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

  1. 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));
    
  2. 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;
      }
    }
    
  3. 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);
      });
    
  4. 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));
    
  5. 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>`;
      }
    }
    
Asincronía, Callbacks y Promesas
Aprende a manejar operaciones asíncronas en JavaScript media...
Almacenamiento en el Navegador
Aprende a almacenar datos en el navegador utilizando localSt...

Conceptos Básicos de HTML

Aprende los conceptos básicos de HTML

Conceptos Básicos de CSS

Aprende los conceptos básicos de CSS

Conceptos Básicos SQL

Aprende los conceptos básicos de SQL

Conceptos Básicos de GIT

Aprende los conceptos básicos de GIT

Conceptos Básicos de Python

Aprende los conceptos básicos de Python

Conceptos Básicos de UML

Aprende los conceptos básicos de UML

Refuerzo Academico de Herramientas de Productividad 2025