TechLead
Principiante
30 min
Lección 3 de 10
API

Fetch API y Peticiones HTTP

Domina la Fetch API para hacer peticiones HTTP en JavaScript con ejemplos prácticos

¿Qué es la Fetch API?

La Fetch API es una interfaz moderna basada en promesas para hacer peticiones HTTP en JavaScript. Está integrada en todos los navegadores modernos y proporciona una alternativa más limpia y poderosa al antiguo enfoque XMLHttpRequest (XHR).

Fetch es nativo de JavaScript—no se requieren bibliotecas externas—y funciona perfectamente con la sintaxis async/await.

✨ Ventajas de Fetch API

🎯
Basada en Promesas

Sintaxis limpia con async/await

📦
Soporte Nativo

Sin dependencias externas

🔄
Streaming

Transmitir cuerpo de respuesta

🛡️
Soporte CORS

Manejo integrado de cross-origin

Petición GET Básica

// Petición GET simple
async function fetchUsers() {
  try {
    const response = await fetch('https://api.example.com/users');
    
    // Verificar si la petición fue exitosa
    if (!response.ok) {
      throw new Error(`¡Error HTTP! Estado: ${response.status}`);
    }
    
    // Parsear respuesta JSON
    const users = await response.json();
    console.log('Usuarios:', users);
    return users;
    
  } catch (error) {
    console.error('Error de Fetch:', error);
  }
}

// Con parámetros de consulta
async function searchUsers(query, page = 1) {
  const params = new URLSearchParams({
    q: query,
    page: page,
    limit: 10
  });
  
  const response = await fetch(`https://api.example.com/users?${params}`);
  return response.json();
}

// Usando constructor URL
async function getUserById(id) {
  const url = new URL(`https://api.example.com/users/${id}`);
  url.searchParams.set('include', 'posts');
  
  const response = await fetch(url);
  return response.json();
}

Petición POST - Creando Datos

// Petición POST con cuerpo JSON
async function createUser(userData) {
  try {
    const response = await fetch('https://api.example.com/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer tu-token-aquí'
      },
      body: JSON.stringify(userData)
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Falló la creación del usuario');
    }

    const newUser = await response.json();
    console.log('Usuario creado:', newUser);
    return newUser;
    
  } catch (error) {
    console.error('Error creando usuario:', error);
    throw error;
  }
}

// Uso
createUser({
  name: 'Juan Pérez',
  email: 'juan@example.com',
  role: 'admin'
});

// POST con datos de formulario
async function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);
  formData.append('description', 'Foto de perfil');

  const response = await fetch('https://api.example.com/upload', {
    method: 'POST',
    // No establecer Content-Type para FormData - el navegador lo establece automáticamente
    body: formData
  });

  return response.json();
}

Peticiones PUT, PATCH y DELETE

// PUT - Reemplazar recurso completo
async function updateUser(id, userData) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer tu-token'
    },
    body: JSON.stringify(userData)
  });

  if (!response.ok) {
    throw new Error('Falló la actualización del usuario');
  }

  return response.json();
}

// PATCH - Actualización parcial
async function patchUser(id, updates) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer tu-token'
    },
    body: JSON.stringify(updates)
  });

  return response.json();
}

// Uso: Solo actualizar campos específicos
patchUser(123, { status: 'active' });

// DELETE - Eliminar recurso
async function deleteUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: 'DELETE',
    headers: {
      'Authorization': 'Bearer tu-token'
    }
  });

  if (!response.ok) {
    throw new Error('Falló la eliminación del usuario');
  }

  // DELETE a menudo retorna 204 No Content
  if (response.status === 204) {
    return { success: true };
  }

  return response.json();
}

Opciones de Configuración de Petición

// Referencia completa de opciones de fetch
const response = await fetch(url, {
  // Método HTTP
  method: 'POST',
  
  // Encabezados de petición
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token',
    'Accept': 'application/json',
    'X-Custom-Header': 'value'
  },
  
  // Cuerpo de petición (para POST, PUT, PATCH)
  body: JSON.stringify(data),
  
  // Modo de petición
  mode: 'cors',          // cors, no-cors, same-origin
  
  // Inclusión de credenciales
  credentials: 'include', // include, same-origin, omit
  
  // Modo de caché
  cache: 'default',      // default, no-store, reload, no-cache, force-cache
  
  // Comportamiento de redirección
  redirect: 'follow',    // follow, error, manual
  
  // Política de referrer
  referrerPolicy: 'no-referrer-when-downgrade',
  
  // Señal de AbortController para cancelación
  signal: controller.signal,
  
  // Mantener conexión viva después de descargar página
  keepalive: false
});

Manejo de Respuesta

// Propiedades y métodos del objeto Response
async function handleResponse() {
  const response = await fetch('https://api.example.com/data');

  // Propiedades de Response
  console.log('Estado:', response.status);         // 200
  console.log('Texto de Estado:', response.statusText); // "OK"
  console.log('¿OK?:', response.ok);                // true (si 200-299)
  console.log('Encabezados:', response.headers);
  console.log('URL:', response.url);
  console.log('Tipo:', response.type);             // "cors", "basic", etc.
  console.log('Redirigido:', response.redirected);

  // Leer encabezados
  const contentType = response.headers.get('Content-Type');
  const rateLimit = response.headers.get('X-RateLimit-Remaining');

  // Iterar encabezados
  for (const [key, value] of response.headers) {
    console.log(`${key}: ${value}`);
  }

  // Métodos del cuerpo de respuesta (¡solo se pueden leer UNA VEZ!)
  // Elige el apropiado según el tipo de contenido:

  // Para JSON
  const json = await response.json();

  // Para texto plano
  const text = await response.text();

  // Para datos binarios (imágenes, archivos)
  const blob = await response.blob();

  // Para binarios como ArrayBuffer
  const buffer = await response.arrayBuffer();

  // Para datos de formulario
  const formData = await response.formData();
}

// Clonar respuesta para leer cuerpo múltiples veces
async function readMultipleTimes() {
  const response = await fetch(url);
  
  // Clonar antes de leer
  const clone = response.clone();
  
  const text = await response.text();
  const json = await clone.json(); // Todavía se puede leer el clon
}

Manejo de Errores

// Manejo integral de errores
async function fetchWithErrorHandling(url, options = {}) {
  try {
    const response = await fetch(url, options);

    // ¡Fetch solo rechaza en errores de red, no en errores HTTP!
    // Necesitamos verificar response.ok manualmente
    
    if (!response.ok) {
      // Intentar obtener detalles del error del cuerpo de respuesta
      let errorMessage = `¡Error HTTP! Estado: ${response.status}`;
      
      try {
        const errorData = await response.json();
        errorMessage = errorData.message || errorMessage;
      } catch {
        // El cuerpo de respuesta no era JSON
      }

      // Crear error personalizado con detalles
      const error = new Error(errorMessage);
      error.status = response.status;
      error.statusText = response.statusText;
      throw error;
    }

    return await response.json();
    
  } catch (error) {
    // Errores de red (sin internet, fallo de DNS, etc.)
    if (error.name === 'TypeError') {
      console.error('Error de red:', error.message);
      throw new Error('No se puede conectar al servidor');
    }
    
    // Errores de aborto
    if (error.name === 'AbortError') {
      console.error('Petición cancelada');
      throw new Error('Petición cancelada');
    }
    
    // Re-lanzar otros errores
    throw error;
  }
}

// Uso con manejo específico de errores
async function loadData() {
  try {
    const data = await fetchWithErrorHandling('/api/data');
    console.log('Datos cargados:', data);
  } catch (error) {
    if (error.status === 401) {
      // Redirigir al login
      window.location.href = '/login';
    } else if (error.status === 404) {
      // Mostrar mensaje de no encontrado
      showNotFound();
    } else {
      // Error genérico
      showError(error.message);
    }
  }
}

Cancelación de Petición con AbortController

// Cancelando peticiones con AbortController
async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      signal: controller.signal
    });
    
    clearTimeout(timeoutId);
    return await response.json();
    
  } catch (error) {
    clearTimeout(timeoutId);
    
    if (error.name === 'AbortError') {
      throw new Error('Tiempo de espera de petición agotado');
    }
    throw error;
  }
}

// Cancelar en acción del usuario
class SearchController {
  constructor() {
    this.controller = null;
  }

  async search(query) {
    // Cancelar petición anterior
    if (this.controller) {
      this.controller.abort();
    }

    // Crear nuevo controller para esta petición
    this.controller = new AbortController();

    try {
      const response = await fetch(`/api/search?q=${query}`, {
        signal: this.controller.signal
      });
      return await response.json();
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('Búsqueda anterior cancelada');
        return null;
      }
      throw error;
    }
  }
}

// Uso con entrada de búsqueda
const searchController = new SearchController();

searchInput.addEventListener('input', async (e) => {
  const results = await searchController.search(e.target.value);
  if (results) {
    displayResults(results);
  }
});

Peticiones Paralelas y Secuenciales

// Peticiones paralelas con Promise.all
async function fetchParallel() {
  try {
    const [users, posts, comments] = await Promise.all([
      fetch('/api/users').then(r => r.json()),
      fetch('/api/posts').then(r => r.json()),
      fetch('/api/comments').then(r => r.json())
    ]);

    console.log('Usuarios:', users);
    console.log('Posts:', posts);
    console.log('Comentarios:', comments);
    
    return { users, posts, comments };
  } catch (error) {
    // Un fallo = todos fallan
    console.error('Una petición falló:', error);
  }
}

// Paralelo con manejo de fallos parciales
async function fetchWithPartialSuccess() {
  const results = await Promise.allSettled([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json())
  ]);

  return results.map((result, index) => {
    if (result.status === 'fulfilled') {
      return { success: true, data: result.value };
    }
    return { success: false, error: result.reason };
  });
}

// Peticiones secuenciales (cuando el orden importa)
async function fetchSequential() {
  // Obtener usuario primero
  const userResponse = await fetch('/api/users/1');
  const user = await userResponse.json();

  // Luego obtener posts del usuario
  const postsResponse = await fetch(`/api/users/${user.id}/posts`);
  const posts = await postsResponse.json();

  // Luego obtener comentarios para cada post
  const postsWithComments = await Promise.all(
    posts.map(async (post) => {
      const commentsResponse = await fetch(`/api/posts/${post.id}/comments`);
      const comments = await commentsResponse.json();
      return { ...post, comments };
    })
  );

  return { user, posts: postsWithComments };
}

Wrapper de Fetch Reutilizable

// Wrapper de fetch listo para producción
class HttpClient {
  constructor(baseUrl, defaultOptions = {}) {
    this.baseUrl = baseUrl;
    this.defaultOptions = {
      headers: {
        'Content-Type': 'application/json'
      },
      ...defaultOptions
    };
  }

  setAuthToken(token) {
    this.defaultOptions.headers['Authorization'] = `Bearer ${token}`;
  }

  removeAuthToken() {
    delete this.defaultOptions.headers['Authorization'];
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseUrl}${endpoint}`;
    
    const mergedOptions = {
      ...this.defaultOptions,
      ...options,
      headers: {
        ...this.defaultOptions.headers,
        ...options.headers
      }
    };

    if (options.body && typeof options.body === 'object') {
      mergedOptions.body = JSON.stringify(options.body);
    }

    const response = await fetch(url, mergedOptions);

    if (!response.ok) {
      const error = new Error('Petición fallida');
      error.status = response.status;
      try {
        error.data = await response.json();
      } catch {}
      throw error;
    }

    if (response.status === 204) {
      return null;
    }

    return response.json();
  }

  get(endpoint, options) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }

  post(endpoint, body, options) {
    return this.request(endpoint, { ...options, method: 'POST', body });
  }

  put(endpoint, body, options) {
    return this.request(endpoint, { ...options, method: 'PUT', body });
  }

  patch(endpoint, body, options) {
    return this.request(endpoint, { ...options, method: 'PATCH', body });
  }

  delete(endpoint, options) {
    return this.request(endpoint, { ...options, method: 'DELETE' });
  }
}

// Uso
const api = new HttpClient('https://api.example.com/v1');
api.setAuthToken('jwt-token-aquí');

// Hacer peticiones
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'Juan', email: 'juan@test.com' });
await api.patch(`/users/${newUser.id}`, { status: 'active' });
await api.delete(`/users/${newUser.id}`);

💡 Mejores Prácticas de Fetch API

  • Siempre verifica response.ok - Fetch no rechaza en errores HTTP
  • Usa try/catch - Maneja errores de red y HTTP por separado
  • Establece encabezados apropiados - Content-Type, Authorization, etc.
  • Usa AbortController - Cancela peticiones obsoletas
  • Crea wrappers reutilizables - Código DRY con manejo consistente de errores
  • Usa URLSearchParams - Para construcción de query strings