Learning Objectives

  • Implement caching layer with TTL
  • Add rate limiting to prevent throttling
  • Build comprehensive error handling
  • Create logging and monitoring

Caching Layer

src/cache/index.js

export class Cache {
  constructor(options = {}) {
    this.cache = new Map();
    this.ttl = options.ttl || 300000; // 5 minutes default
    this.maxSize = options.maxSize || 100;
  }
  
  get(key) {
    const entry = this.cache.get(key);
    
    if (!entry) {
      return null;
    }
    
    // Check if expired
    if (Date.now() - entry.timestamp > this.ttl) {
      this.cache.delete(key);
      return null;
    }
    
    return entry.value;
  }
  
  set(key, value) {
    // Evict oldest if at capacity
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    this.cache.set(key, {
      value,
      timestamp: Date.now()
    });
  }
  
  has(key) {
    return this.get(key) !== null;
  }
  
  delete(key) {
    this.cache.delete(key);
  }
  
  clear() {
    this.cache.clear();
  }
  
  getStats() {
    return {
      size: this.cache.size,
      maxSize: this.maxSize,
      ttl: this.ttl
    };
  }
}

Rate Limiter

src/utils/rateLimiter.js

export class RateLimiter {
  constructor(requestsPerSecond) {
    this.requestsPerSecond = requestsPerSecond;
    this.interval = 1000 / requestsPerSecond;
    this.lastCallTime = 0;
    this.queue = [];
    this.processing = false;
  }
  
  async execute(fn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ fn, resolve, reject });
      this.processQueue();
    });
  }
  
  async processQueue() {
    if (this.processing || this.queue.length === 0) {
      return;
    }
    
    this.processing = true;
    
    while (this.queue.length > 0) {
      const now = Date.now();
      const timeSinceLastCall = now - this.lastCallTime;
      
      if (timeSinceLastCall < this.interval) {
        const delay = this.interval - timeSinceLastCall;
        await new Promise(resolve => setTimeout(resolve, delay));
      }
      
      const { fn, resolve, reject } = this.queue.shift();
      this.lastCallTime = Date.now();
      
      try {
        const result = await fn();
        resolve(result);
      } catch (error) {
        reject(error);
      }
    }
    
    this.processing = false;
  }
}

Enhanced API Client with Caching

src/api/cachedClient.js

import { APIClient } from './client.js';
import { Cache } from '../cache/index.js';
import { RateLimiter } from '../utils/rateLimiter.js';

export class CachedAPIClient extends APIClient {
  constructor(baseURL, options = {}) {
    super(baseURL, options);
    
    this.cache = new Cache({
      ttl: options.cacheTTL || 300000,
      maxSize: options.cacheMaxSize || 100
    });
    
    this.rateLimiter = new RateLimiter(
      options.requestsPerSecond || 10
    );
    
    this.stats = {
      requests: 0,
      cacheHits: 0,
      cacheMisses: 0,
      errors: 0
    };
  }
  
  async request(endpoint, options = {}) {
    const cacheKey = this.getCacheKey(endpoint, options);
    
    // Check cache for GET requests
    if (!options.method || options.method === 'GET') {
      const cached = this.cache.get(cacheKey);
      
      if (cached) {
        this.stats.cacheHits++;
        console.log(`Cache hit: ${endpoint}`);
        return cached;
      }
      
      this.stats.cacheMisses++;
    }
    
    // Rate limit the request
    return await this.rateLimiter.execute(async () => {
      try {
        this.stats.requests++;
        const data = await super.request(endpoint, options);
        
        // Cache GET responses
        if (!options.method || options.method === 'GET') {
          this.cache.set(cacheKey, data);
        }
        
        return data;
      } catch (error) {
        this.stats.errors++;
        throw error;
      }
    });
  }
  
  getCacheKey(endpoint, options) {
    const method = options.method || 'GET';
    return `${method}:${endpoint}`;
  }
  
  invalidateCache(pattern) {
    if (typeof pattern === 'string') {
      this.cache.delete(pattern);
    }
  }
  
  getStats() {
    return {
      ...this.stats,
      cache: this.cache.getStats(),
      cacheHitRate: (
        (this.stats.cacheHits / (this.stats.cacheHits + this.stats.cacheMisses)) * 100
      ).toFixed(2) + '%'
    };
  }
}

Logger

src/utils/logger.js

export class Logger {
  constructor(options = {}) {
    this.level = options.level || 'info';
    this.levels = {
      error: 0,
      warn: 1,
      info: 2,
      debug: 3
    };
  }
  
  log(level, message, data = {}) {
    if (this.levels[level] <= this.levels[this.level]) {
      const timestamp = new Date().toISOString();
      const logEntry = {
        timestamp,
        level: level.toUpperCase(),
        message,
        ...data
      };
      
      console.log(JSON.stringify(logEntry, null, 2));
    }
  }
  
  error(message, data) {
    this.log('error', message, data);
  }
  
  warn(message, data) {
    this.log('warn', message, data);
  }
  
  info(message, data) {
    this.log('info', message, data);
  }
  
  debug(message, data) {
    this.log('debug', message, data);
  }
}

Enhanced Aggregator

src/aggregator/enhanced.js

import { CachedAPIClient } from '../api/cachedClient.js';
import { Logger } from '../utils/logger.js';

export class EnhancedAggregator {
  constructor(options = {}) {
    this.logger = new Logger({ level: options.logLevel || 'info' });
    
    this.weatherAPI = new CachedAPIClient(
      'https://api.weather.example.com',
      {
        timeout: 3000,
        retries: 2,
        cacheTTL: 600000, // 10 minutes
        requestsPerSecond: 5
      }
    );
    
    this.newsAPI = new CachedAPIClient(
      'https://api.news.example.com',
      {
        timeout: 3000,
        retries: 2,
        cacheTTL: 300000, // 5 minutes
        requestsPerSecond: 10
      }
    );
    
    this.stocksAPI = new CachedAPIClient(
      'https://api.stocks.example.com',
      {
        timeout: 3000,
        retries: 2,
        cacheTTL: 60000, // 1 minute
        requestsPerSecond: 20
      }
    );
  }
  
  async fetchAll(config) {
    this.logger.info('Starting data aggregation', { config });
    
    const startTime = Date.now();
    
    try {
      const results = await Promise.allSettled([
        this.fetchWeather(config.city),
        this.fetchNews(config.newsCategory),
        this.fetchStocks(config.stockSymbol)
      ]);
      
      const data = {
        weather: this.extractResult(results[0], 'weather'),
        news: this.extractResult(results[1], 'news'),
        stocks: this.extractResult(results[2], 'stocks')
      };
      
      const duration = Date.now() - startTime;
      
      this.logger.info('Aggregation complete', {
        duration: `${duration}ms`,
        success: results.filter(r => r.status === 'fulfilled').length,
        failed: results.filter(r => r.status === 'rejected').length
      });
      
      return {
        success: true,
        data,
        metadata: {
          duration,
          timestamp: new Date().toISOString(),
          stats: this.getStats()
        }
      };
    } catch (error) {
      this.logger.error('Aggregation failed', { error: error.message });
      
      return {
        success: false,
        error: error.message,
        timestamp: new Date().toISOString()
      };
    }
  }
  
  extractResult(result, source) {
    if (result.status === 'fulfilled') {
      return result.value;
    }
    
    this.logger.warn(`Failed to fetch ${source}`, {
      error: result.reason.message
    });
    
    return null;
  }
  
  async fetchWeather(city) {
    // Mock implementation - replace with actual API call
    return {
      city,
      temperature: Math.round(Math.random() * 30 + 10),
      condition: 'Sunny',
      timestamp: new Date().toISOString()
    };
  }
  
  async fetchNews(category) {
    // Mock implementation
    return {
      category,
      articles: [
        { title: 'News 1', source: 'Source 1' },
        { title: 'News 2', source: 'Source 2' }
      ],
      timestamp: new Date().toISOString()
    };
  }
  
  async fetchStocks(symbol) {
    // Mock implementation
    return {
      symbol,
      price: (100 + Math.random() * 10).toFixed(2),
      change: (Math.random() - 0.5).toFixed(2),
      timestamp: new Date().toISOString()
    };
  }
  
  getStats() {
    return {
      weather: this.weatherAPI.getStats(),
      news: this.newsAPI.getStats(),
      stocks: this.stocksAPI.getStats()
    };
  }
  
  clearCache() {
    this.weatherAPI.cache.clear();
    this.newsAPI.cache.clear();
    this.stocksAPI.cache.clear();
    this.logger.info('Cache cleared');
  }
}

Updated Main Entry

src/index.js

import { EnhancedAggregator } from './aggregator/enhanced.js';

async function main() {
  const aggregator = new EnhancedAggregator({
    logLevel: 'info'
  });
  
  const config = {
    city: 'London',
    newsCategory: 'technology',
    stockSymbol: 'AAPL'
  };
  
  console.log('=== Data Aggregator ===\n');
  
  // First fetch
  console.log('First fetch (no cache)...');
  const result1 = await aggregator.fetchAll(config);
  console.log('\nResult:', JSON.stringify(result1, null, 2));
  
  // Second fetch (should use cache)
  console.log('\n\nSecond fetch (with cache)...');
  const result2 = await aggregator.fetchAll(config);
  console.log('\nResult:', JSON.stringify(result2, null, 2));
  
  // Print statistics
  console.log('\n\n=== Statistics ===');
  console.log(JSON.stringify(aggregator.getStats(), null, 2));
}

main().catch(console.error);

Integration Tests

tests/aggregator.test.js

import { EnhancedAggregator } from '../src/aggregator/enhanced.js';

describe('EnhancedAggregator', () => {
  let aggregator;
  
  beforeEach(() => {
    aggregator = new EnhancedAggregator({ logLevel: 'error' });
  });
  
  test('fetches all data successfully', async () => {
    const config = {
      city: 'London',
      newsCategory: 'tech',
      stockSymbol: 'AAPL'
    };
    
    const result = await aggregator.fetchAll(config);
    
    expect(result.success).toBe(true);
    expect(result.data.weather).toBeDefined();
    expect(result.data.news).toBeDefined();
    expect(result.data.stocks).toBeDefined();
  });
  
  test('uses cache on second request', async () => {
    const config = {
      city: 'London',
      newsCategory: 'tech',
      stockSymbol: 'AAPL'
    };
    
    await aggregator.fetchAll(config);
    const result2 = await aggregator.fetchAll(config);
    
    const stats = aggregator.getStats();
    expect(stats.weather.cacheHits).toBeGreaterThan(0);
  });
});

Key Takeaways

  • ✅ Caching layer with TTL reduces API calls
  • ✅ Rate limiting prevents API throttling
  • ✅ Comprehensive logging for debugging
  • ✅ Statistics tracking for monitoring
  • ✅ Graceful error handling with fallbacks
  • ✅ Integration tests verify functionality

Next Steps

In Part 3, we'll add final testing and deployment preparation!