💡 Best Practices y Rate-Limits

Evita errores 429, cachea respuestas y gestiona tokens de acceso automáticamente

📋 ¿Qué aprenderás?

🚀 Guía Paso a Paso

1

Implementar Rate Limiting con Backoff Exponencial

Evita ser bloqueado por hacer demasiadas peticiones:

// Gestor de rate limits con backoff exponencial class RateLimitManager { constructor(maxRetries = 3, baseDelay = 1000) { this.maxRetries = maxRetries; this.baseDelay = baseDelay; this.requestQueue = []; this.isProcessing = false; } async makeRequest(requestFunc, retryCount = 0) { try { const response = await requestFunc(); // Verificar headers de rate limit const remaining = response.headers.get('X-RateLimit-Remaining'); const reset = response.headers.get('X-RateLimit-Reset'); if (remaining && parseInt(remaining) < 5) { console.warn(`⚠️ Rate limit bajo: ${remaining} requests restantes`); await this.waitForReset(reset); } return response; } catch (error) { if (error.status === 429 && retryCount < this.maxRetries) { const retryAfter = error.headers.get('Retry-After'); const delay = retryAfter ? parseInt(retryAfter) * 1000 : this.calculateBackoff(retryCount); console.log(`⏳ Rate limited, esperando ${delay}ms antes de reintento ${retryCount + 1}`); await this.delay(delay); return this.makeRequest(requestFunc, retryCount + 1); } throw error; } } calculateBackoff(retryCount) { // Backoff exponencial con jitter para evitar thundering herd const baseDelay = this.baseDelay * Math.pow(2, retryCount); const jitter = Math.random() * 0.1 * baseDelay; // 10% jitter return baseDelay + jitter; } async waitForReset(resetTimestamp) { const resetTime = new Date(resetTimestamp * 1000); const waitTime = resetTime - Date.now(); if (waitTime > 0) { console.log(`⏰ Esperando hasta ${resetTime.toLocaleTimeString()} para reset de rate limit`); await this.delay(waitTime); } } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } // Uso const rateLimiter = new RateLimitManager(5, 1000); // Petición con rate limiting automático const response = await rateLimiter.makeRequest(async () => { return fetch('https://api.spotify.com/v1/search?q=test&type=track', { headers: { 'Authorization': `Bearer ${token}` } }); });
2

Implementar Sistema de Caché Inteligente

Reduce peticiones innecesarias y mejora el rendimiento:

// Sistema de caché avanzado con TTL y validación class APICache { constructor(defaultTTL = 300000) { // 5 minutos por defecto this.cache = new Map(); this.defaultTTL = defaultTTL; this.stats = { hits: 0, misses: 0, evictions: 0 }; } // Generar clave única para la petición generateKey(url, params = {}) { const sortedParams = Object.keys(params).sort().reduce((sorted, key) => { sorted[key] = params[key]; return sorted; }, {}); return `${url}?${JSON.stringify(sortedParams)}`; } // Guardar en caché con TTL set(key, data, ttl = this.defaultTTL) { const now = Date.now(); const expires = now + ttl; this.cache.set(key, { data: data, expires: expires, created: now, accessCount: 0 }); // Programar eliminación automática setTimeout(() => { this.delete(key); this.stats.evictions++; }, ttl); } // Obtener de caché get(key) { const item = this.cache.get(key); if (!item) { this.stats.misses++; return null; } // Verificar expiración if (Date.now() > item.expires) { this.delete(key); this.stats.misses++; return null; } // Actualizar estadísticas item.accessCount++; this.stats.hits++; return item.data; } // Eliminar de caché delete(key) { return this.cache.delete(key); } // Limpiar caché expirada clearExpired() { const now = Date.now(); let cleared = 0; for (const [key, item] of this.cache) { if (now > item.expires) { this.delete(key); cleared++; } } console.log(`🧹 Caché limpiada: ${cleared} elementos eliminados`); return cleared; } // TTL dinámico basado en tipo de dato getTTLForEndpoint(endpoint) { const ttlRules = { 'search': 300000, // 5 minutos para búsquedas 'user_profile': 900000, // 15 minutos para perfiles 'audio_features': 600000, // 10 minutos para features 'charts': 1800000, // 30 minutos para charts 'default': 300000 // 5 minutos por defecto }; for (const [pattern, ttl] of Object.entries(ttlRules)) { if (endpoint.includes(pattern)) { return ttl; } } return ttlRules.default; } // Obtener estadísticas getStats() { return { ...this.stats, size: this.cache.size, hitRate: this.stats.hits / (this.stats.hits + this.stats.misses) || 0 }; } } // Caché con lógica de invalidación inteligente class SmartAPICache extends APICache { constructor(defaultTTL = 300000, maxSize = 1000) { super(defaultTTL); this.maxSize = maxSize; this.accessOrder = []; } set(key, data, ttl = this.defaultTTL) { // Verificar tamaño máximo if (this.cache.size >= this.maxSize) { this.evictLeastUsed(); } super.set(key, data, ttl); this.accessOrder.push(key); } get(key) { const result = super.get(key); if (result) { // Actualizar orden de acceso this.accessOrder = this.accessOrder.filter(k => k !== key); this.accessOrder.push(key); } return result; } // Evictar el menos usado evictLeastUsed() { if (this.accessOrder.length > 0) { const oldestKey = this.accessOrder[0]; this.delete(oldestKey); this.accessOrder.shift(); } } } // Uso con integración automática class CachedAPIClient { constructor(apiKey) { this.apiKey = apiKey; this.cache = new SmartAPICache(); this.rateLimiter = new RateLimitManager(); } async makeCachedRequest(endpoint, params = {}, useCache = true) { const cacheKey = this.cache.generateKey(endpoint, params); // Intentar obtener de caché if (useCache) { const cached = this.cache.get(cacheKey); if (cached) { console.log('✅ Cache hit:', cacheKey); return cached; } } console.log('🔄 Cache miss, haciendo petición:', cacheKey); // Hacer petición con rate limiting const response = await this.rateLimiter.makeRequest(async () => { return fetch(`${endpoint}?${new URLSearchParams(params)}`, { headers: { 'Authorization': `Bearer ${this.apiKey}` } }); }); const data = await response.json(); // Guardar en caché con TTL apropiado const ttl = this.cache.getTTLForEndpoint(endpoint); this.cache.set(cacheKey, data, ttl); return data; } }
3

Gestión Automática de Tokens de Acceso

Refresca tokens automáticamente y mantén sesiones activas:

// Gestor de tokens con auto-refresh class TokenManager { constructor() { this.tokens = new Map(); this.refreshCallbacks = new Map(); } // Registrar token con información de expiración registerToken(service, tokenData) { const now = Date.now(); const expiresAt = tokenData.expires_at || (now + (tokenData.expires_in * 1000)); const refreshAt = expiresAt - 60000; // Refrescar 1 minuto antes this.tokens.set(service, { access_token: tokenData.access_token, refresh_token: tokenData.refresh_token, expires_at: expiresAt, refresh_at: refreshAt, service: service }); // Programar refresh automático this.scheduleTokenRefresh(service, refreshAt); console.log(`🔑 Token registrado para ${service}, expira: ${new Date(expiresAt).toLocaleTimeString()}`); } // Programar refresh automático scheduleTokenRefresh(service, refreshAt) { const delay = refreshAt - Date.now(); if (delay > 0) { setTimeout(async () => { await this.refreshToken(service); }, delay); console.log(`⏰ Refresh programado para ${service} en ${Math.round(delay / 1000)}s`); } else { // Si ya expiró, refrescar inmediatamente this.refreshToken(service); } } // Refrescar token async refreshToken(service) { const tokenData = this.tokens.get(service); if (!tokenData || !tokenData.refresh_token) { console.error(`❌ No hay refresh token para ${service}`); return false; } try { console.log(`🔄 Refrescando token para ${service}...`); const response = await fetch(this.getRefreshEndpoint(service), { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: tokenData.refresh_token, client_id: this.getClientId(service), client_secret: this.getClientSecret(service) }) }); if (!response.ok) { throw new Error(`Error refrescando token: ${response.status}`); } const newTokenData = await response.json(); // Actualizar con nuevos tokens this.registerToken(service, { ...newTokenData, refresh_token: newTokenData.refresh_token || tokenData.refresh_token }); // Ejecutar callbacks const callbacks = this.refreshCallbacks.get(service) || []; callbacks.forEach(callback => callback(newTokenData.access_token)); console.log(`✅ Token refrescado para ${service}`); return true; } catch (error) { console.error(`❌ Error refrescando token para ${service}:`, error); // Notificar a callbacks de error const callbacks = this.refreshCallbacks.get(service) || []; callbacks.forEach(callback => callback(null, error)); return false; } } // Obtener token válido getValidToken(service) { const tokenData = this.tokens.get(service); if (!tokenData) { throw new Error(`No hay token registrado para ${service}`); } if (Date.now() >= tokenData.expires_at) { throw new Error(`Token expirado para ${service}`); } return tokenData.access_token; } // Registrar callback para cambios de token onTokenRefresh(service, callback) { if (!this.refreshCallbacks.has(service)) { this.refreshCallbacks.set(service, []); } this.refreshCallbacks.get(service).push(callback); } // Métodos auxiliares (implementar según el servicio) getRefreshEndpoint(service) { const endpoints = { 'spotify': 'https://accounts.spotify.com/api/token', 'google': 'https://oauth2.googleapis.com/token', 'github': 'https://github.com/login/oauth/access_token' }; return endpoints[service]; } getClientId(service) { // Obtener de variables de entorno o configuración return process.env[`${service.toUpperCase()}_CLIENT_ID`]; } getClientSecret(service) { // Obtener de variables de entorno o configuración return process.env[`${service.toUpperCase()}_CLIENT_SECRET`]; } } // Cliente API con gestión automática de tokens class TokenAwareAPIClient { constructor(tokenManager, service) { this.tokenManager = tokenManager; this.service = service; } async makeRequest(url, options = {}) { try { const token = this.tokenManager.getValidToken(this.service); const response = await fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token}` } }); if (response.status === 401) { // Token expirado, intentar refresh console.log('🔄 Token expirado, intentando refresh...'); const refreshed = await this.tokenManager.refreshToken(this.service); if (refreshed) { // Reintentar con nuevo token const newToken = this.tokenManager.getValidToken(this.service); return fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${newToken}` } }); } } return response; } catch (error) { console.error(`❌ Error en petición para ${this.service}:`, error); throw error; } } } // Uso completo con gestión automática const tokenManager = new TokenManager(); // Registrar tokens iniciales tokenManager.registerToken('spotify', { access_token: 'initial_access_token', refresh_token: 'initial_refresh_token', expires_in: 3600 }); // Crear cliente con gestión automática const spotifyClient = new TokenAwareAPIClient(tokenManager, 'spotify'); // Callback para cuando el token se refresque tokenManager.onTokenRefresh('spotify', (newToken) => { console.log('📢 Token de Spotify refrescado:', newToken.substring(0, 10) + '...'); // Actualizar UI, reinicar reproducción, etc. }); // Hacer peticiones sin preocuparse por tokens const data = await spotifyClient.makeRequest('https://api.spotify.com/v1/me');
4

Optimización de Rendimiento y Concurrencia

Maneja múltiples peticiones eficientemente:

// Gestor de peticiones concurrentes con control de carga class ConcurrentRequestManager { constructor(maxConcurrent = 5, requestsPerSecond = 10) { this.maxConcurrent = maxConcurrent; this.requestsPerSecond = requestsPerSecond; this.activeRequests = 0; this.queue = []; this.requestTimestamps = []; } async addRequest(requestFunc, priority = 0) { return new Promise((resolve, reject) => { this.queue.push({ func: requestFunc, priority: priority, resolve: resolve, reject: reject, timestamp: Date.now() }); // Ordenar por prioridad this.queue.sort((a, b) => b.priority - a.priority); this.processQueue(); }); } async processQueue() { while (this.queue.length > 0 && this.canMakeRequest()) { const request = this.queue.shift(); this.executeRequest(request); } } canMakeRequest() { // Verificar límite de concurrentes if (this.activeRequests >= this.maxConcurrent) { return false; } // Verificar límite de rate this.cleanupTimestamps(); return this.requestTimestamps.length < this.requestsPerSecond; } cleanupTimestamps() { const oneSecondAgo = Date.now() - 1000; this.requestTimestamps = this.requestTimestamps.filter( timestamp => timestamp > oneSecondAgo ); } async executeRequest(request) { this.activeRequests++; this.requestTimestamps.push(Date.now()); try { const result = await request.func(); request.resolve(result); } catch (error) { request.reject(error); } finally { this.activeRequests--; this.processQueue(); // Procesar siguiente en cola } } // Método conveniente para peticiones múltiples async executeAll(requests, priority = 0) { const promises = requests.map(requestFunc => this.addRequest(requestFunc, priority) ); return Promise.all(promises); } // Obtener estadísticas getStats() { return { activeRequests: this.activeRequests, queueSize: this.queue.length, requestsInLastSecond: this.requestTimestamps.length, maxConcurrent: this.maxConcurrent, requestsPerSecond: this.requestsPerSecond }; } } // Optimización de peticiones múltiples con deduplicación class OptimizedAPIClient { constructor(concurrentManager, cache) { this.concurrentManager = concurrentManager; this.cache = cache; this.pendingRequests = new Map(); // Deduplicación de peticiones } async fetchWithOptimization(endpoint, params = {}, options = {}) { const cacheKey = this.cache.generateKey(endpoint, params); // Verificar caché if (options.useCache !== false) { const cached = this.cache.get(cacheKey); if (cached) return cached; } // Verificar si ya hay una petición pendiente if (this.pendingRequests.has(cacheKey)) { console.log('🔄 Petición duplicada detectada, esperando resultado...'); return this.pendingRequests.get(cacheKey); } // Crear petición const requestPromise = this.concurrentManager.addRequest(async () => { const response = await fetch(`${endpoint}?${new URLSearchParams(params)}`, { headers: options.headers || {} }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); }, options.priority || 0); // Guardar como petición pendiente this.pendingRequests.set(cacheKey, requestPromise); try { const result = await requestPromise; // Guardar en caché if (options.useCache !== false) { const ttl = this.cache.getTTLForEndpoint(endpoint); this.cache.set(cacheKey, result, ttl); } // Limpiar petición pendiente this.pendingRequests.delete(cacheKey); return result; } catch (error) { // Limpiar en caso de error this.pendingRequests.delete(cacheKey); throw error; } } } // Uso con optimizaciones completas const concurrentManager = new ConcurrentRequestManager(10, 20); const cache = new SmartAPICache(); const optimizedClient = new OptimizedAPIClient(concurrentManager, cache); // Ejemplo: Buscar múltiples artistas eficientemente async function searchMultipleArtists(artistNames) { const searches = artistNames.map(name => optimizedClient.fetchWithOptimization('https://api.spotify.com/v1/search', { q: name, type: 'artist', limit: 1 }, { priority: 1, // Alta prioridad useCache: true }) ); const results = await Promise.all(searches); return results.map(result => result.artists.items[0]).filter(Boolean); } // Ejemplo con manejo de errores inteligente async function robustSearch(query) { try { const result = await optimizedClient.fetchWithOptimization( 'https://api.spotify.com/v1/search', { q: query, type: 'track', limit: 10 }, { priority: 2, useCache: true } ); return result; } catch (error) { if (error.message.includes('429')) { console.log('🔄 Rate limit alcanzado, reintentando con backoff...'); // Esperar y reintentar con menor prioridad await new Promise(resolve => setTimeout(resolve, 5000)); return optimizedClient.fetchWithOptimization( 'https://api.spotify.com/v1/search', { q: query, type: 'track', limit: 5 }, // Menos resultados { priority: 0, useCache: true } // Menor prioridad ); } throw error; } }
5

Monitoreo y Métricas de Rendimiento

Supervisa el rendimiento y detecta problemas:

// Sistema de monitoreo y métricas class APIMonitor { constructor() { this.metrics = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, rateLimitedRequests: 0, cacheHits: 0, cacheMisses: 0, averageResponseTime: 0, requestsPerMinute: [], errorsByType: new Map() }; this.startTime = Date.now(); this.responseTimes = []; } // Registrar métricas de petición recordRequest(endpoint, startTime, success, error = null, cached = false) { const duration = Date.now() - startTime; this.metrics.totalRequests++; this.responseTimes.push(duration); if (success) { this.metrics.successfulRequests++; } else { this.metrics.failedRequests++; this.recordError(error); } if (cached) { this.metrics.cacheHits++; } else { this.metrics.cacheMisses++; } // Actualizar tiempo promedio de respuesta this.updateAverageResponseTime(); // Registrar por minuto this.recordRequestsPerMinute(); // Log detallado console.log(`📊 ${endpoint}: ${duration}ms, ${success ? '✅' : '❌'}, ${cached ? '📦 cached' : '🌐 fresh'}`); } recordError(error) { if (error.status === 429) { this.metrics.rateLimitedRequests++; } const errorType = error.message || 'Unknown'; this.metrics.errorsByType.set(errorType, (this.metrics.errorsByType.get(errorType) || 0) + 1 ); } updateAverageResponseTime() { const recentTimes = this.responseTimes.slice(-100); // Últimas 100 peticiones this.metrics.averageResponseTime = recentTimes.reduce((sum, time) => sum + time, 0) / recentTimes.length; } recordRequestsPerMinute() { const now = Date.now(); const oneMinuteAgo = now - 60000; // Limpiar timestamps antiguos this.metrics.requestsPerMinute = this.metrics.requestsPerMinute.filter( timestamp => timestamp > oneMinuteAgo ); // Añadir timestamp actual this.metrics.requestsPerMinute.push(now); } // Obtener estadísticas actuales getStats() { const uptime = Date.now() - this.startTime; const currentRPM = this.metrics.requestsPerMinute.length; return { uptime: Math.floor(uptime / 1000), // segundos totalRequests: this.metrics.totalRequests, successRate: (this.metrics.successfulRequests / this.metrics.totalRequests * 100).toFixed(2), errorRate: (this.metrics.failedRequests / this.metrics.totalRequests * 100).toFixed(2), rateLimitRate: (this.metrics.rateLimitedRequests / this.metrics.totalRequests * 100).toFixed(2), cacheHitRate: (this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses) * 100).toFixed(2), averageResponseTime: Math.round(this.metrics.averageResponseTime), currentRequestsPerMinute: currentRPM, errorsByType: Object.fromEntries(this.metrics.errorsByType), memoryUsage: this.getMemoryUsage() }; } getMemoryUsage() { if (performance.memory) { return { used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024), total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024), limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024) }; } return null; } // Alertas de salud checkHealth() { const stats = this.getStats(); const alerts = []; if (stats.errorRate > 10) { alerts.push(`🚨 Alta tasa de errores: ${stats.errorRate}%`); } if (stats.rateLimitRate > 5) { alerts.push(`⚠️ Muchos rate limits: ${stats.rateLimitRate}%`); } if (stats.averageResponseTime > 1000) { alerts.push(`⏱️ Respuestas lentas: ${stats.averageResponseTime}ms promedio`); } if (stats.currentRequestsPerMinute > 100) { alerts.push(`🔥 Alta carga: ${stats.currentRequestsPerMinute} RPM`); } return alerts; } // Generar reporte generateReport() { const stats = this.getStats(); const health = this.checkHealth(); return { timestamp: new Date().toISOString(), stats: stats, health: { status: health.length === 0 ? 'healthy' : 'degraded', alerts: health }, recommendations: this.generateRecommendations(stats, health) }; } generateRecommendations(stats, health) { const recommendations = []; if (stats.errorRate > 5) { recommendations.push('Revisar logs de errores y endpoints problemáticos'); } if (stats.rateLimitRate > 2) { recommendations.push('Implementar caché más agresivo o reducir frecuencia de peticiones'); } if (stats.cacheHitRate < 50) { recommendations.push('Aumentar TTL del caché o implementar caché predictivo'); } if (stats.averageResponseTime > 500) { recommendations.push('Optimizar peticiones lentas o implementar timeout más estricto'); } return recommendations; } } // Dashboard de monitoreo en tiempo real class MonitoringDashboard { constructor(monitor) { this.monitor = monitor; this.updateInterval = null; } startUpdates(intervalMs = 5000) { this.updateInterval = setInterval(() => { this.updateDashboard(); }, intervalMs); } updateDashboard() { const report = this.monitor.generateReport(); // Actualizar UI con las métricas this.updateMetrics(report.stats); this.updateHealthStatus(report.health); this.updateRecommendations(report.recommendations); console.log('📊 Dashboard actualizado:', report.timestamp); } updateMetrics(stats) { // Actualizar elementos del DOM con las métricas document.getElementById('successRate').textContent = stats.successRate + '%'; document.getElementById('avgResponseTime').textContent = stats.averageResponseTime + 'ms'; document.getElementById('cacheHitRate').textContent = stats.cacheHitRate + '%'; document.getElementById('currentRPM').textContent = stats.currentRequestsPerMinute; } stopUpdates() { if (this.updateInterval) { clearInterval(this.updateInterval); this.updateInterval = null; } } } // Uso completo del sistema de monitoreo const monitor = new APIMonitor(); const dashboard = new MonitoringDashboard(monitor); // Integrar con los clientes anteriores class MonitoredAPIClient extends OptimizedAPIClient { constructor(concurrentManager, cache, monitor) { super(concurrentManager, cache); this.monitor = monitor; } async fetchWithOptimization(endpoint, params = {}, options = {}) { const startTime = Date.now(); try { const result = await super.fetchWithOptimization(endpoint, params, options); this.monitor.recordRequest(endpoint, startTime, true, null, options.useCache !== false); return result; } catch (error) { this.monitor.recordRequest(endpoint, startTime, false, error, options.useCache !== false); throw error; } } }

🚦 Demo: Rate Limiting en Acción

Prueba el sistema de rate limiting con simulación:

0
Peticiones Realizadas
0
Rate Limited
0
Retardo Promedio
100%
Tasa de Éxito

💾 Demo: Sistema de Caché

Prueba el sistema de caché inteligente:

0
Cache Hits
0
Cache Misses
0%
Hit Rate
0
Tamaño Caché

🔑 Demo: Gestión de Tokens

Simula el ciclo de vida de un token:

Activo
Estado del Token
0s
Edad del Token
0
Refrescos Realizados
30s
Próximo Refresh

📊 Resumen de Mejores Prácticas

🔄
Implementa backoff exponencial
💾
Cachea respuestas inteligentemente
🔑
Gestiona tokens automáticamente
📊
Monitorea el rendimiento
← Volver a Tutoriales