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