Practice and reinforce the concepts from Lesson 8
In this activity, you'll:
Total time: 60-75 minutes
Access the template here: Caching System Template
git clone
and navigate to the template folderindex.html
in your browser to start!What you'll do: Pick your favorite API to cache Time: 5 minutes
Choose one of these exciting caching projects:
Cache your favorite Pokemon team and their stats
Cache character data for your personal Star Wars database
Cache weather forecasts for your favorite cities
Cache country data for a travel planning app
What you'll do: Build a flexible localStorage cache Time: 15 minutes
class UniversalCache {
constructor(cachePrefix = 'app_cache_') {
this.cachePrefix = cachePrefix;
this.defaultExpiry = 10 * 60 * 1000; // 10 minutes
}
// Save any data to cache with expiration time
set(key, data, expiryTime = this.defaultExpiry) {
const cacheData = {
data: data,
timestamp: Date.now(),
expiry: Date.now() + expiryTime,
type: this.determineDataType(data)
};
try {
localStorage.setItem(this.cachePrefix + key, JSON.stringify(cacheData));
console.log(`✅ Cached ${cacheData.type}: ${key}`);
return true;
} catch (error) {
console.error('Failed to cache data:', error);
this.handleStorageQuotaExceeded();
return false;
}
}
// Get data from cache if not expired
get(key) {
try {
const cachedItem = localStorage.getItem(this.cachePrefix + key);
if (!cachedItem) {
console.log(`❌ No cache found for: ${key}`);
return null;
}
const cacheData = JSON.parse(cachedItem);
// Check if cache has expired
if (Date.now() > cacheData.expiry) {
console.log(`⏰ Cache expired for: ${key}`);
this.remove(key);
return null;
}
console.log(`✅ Cache hit for: ${key} (${cacheData.type})`);
return cacheData.data;
} catch (error) {
console.error('Failed to read cache:', error);
return null;
}
}
// Determine what type of data we're caching
determineDataType(data) {
if (Array.isArray(data)) return 'Array';
if (data.name && data.stats) return 'Pokemon';
if (data.name && data.height) return 'Character';
if (data.name && data.main) return 'Weather';
if (data.name && data.capital) return 'Country';
return 'Object';
}
// Remove specific cache entry
remove(key) {
localStorage.removeItem(this.cachePrefix + key);
console.log(`🗑️ Removed cache for: ${key}`);
}
// Clear all cache with this prefix
clear() {
Object.keys(localStorage)
.filter(key => key.startsWith(this.cachePrefix))
.forEach(key => localStorage.removeItem(key));
console.log('🧹 Cleared all cache');
}
// Handle storage quota exceeded
handleStorageQuotaExceeded() {
console.warn('⚠️ Storage quota exceeded, clearing old cache...');
this.clearOldestEntries(5); // Remove 5 oldest entries
}
// Clear oldest cache entries
clearOldestEntries(count = 5) {
const cacheItems = Object.keys(localStorage)
.filter(key => key.startsWith(this.cachePrefix))
.map(key => {
const data = JSON.parse(localStorage.getItem(key));
return { key, timestamp: data.timestamp };
})
.sort((a, b) => a.timestamp - b.timestamp)
.slice(0, count);
cacheItems.forEach(item => {
localStorage.removeItem(item.key);
console.log(`🗑️ Auto-removed old cache: ${item.key}`);
});
}
// Get cache statistics
getStats() {
const keys = Object.keys(localStorage).filter(key => key.startsWith(this.cachePrefix));
const stats = {
totalItems: keys.length,
totalSize: 0,
itemsByType: {},
oldestItem: null,
newestItem: null
};
keys.forEach(key => {
const item = localStorage.getItem(key);
stats.totalSize += item.length;
try {
const data = JSON.parse(item);
const type = data.type || 'Unknown';
stats.itemsByType[type] = (stats.itemsByType[type] || 0) + 1;
if (!stats.oldestItem || data.timestamp < stats.oldestItem.timestamp) {
stats.oldestItem = { key, timestamp: data.timestamp };
}
if (!stats.newestItem || data.timestamp > stats.newestItem.timestamp) {
stats.newestItem = { key, timestamp: data.timestamp };
}
} catch (e) {
// Ignore parsing errors
}
});
stats.totalSizeKB = Math.round(stats.totalSize / 1024);
return stats;
}
}
// Create global cache instances for different data types
const pokemonCache = new UniversalCache('pokemon_');
const characterCache = new UniversalCache('character_');
const weatherCache = new UniversalCache('weather_');
const countryCache = new UniversalCache('country_');
// Test cache with different data types
function testUniversalCache() {
console.log('Testing universal cache system...');
// Test Pokemon data
const samplePokemon = {
name: 'pikachu',
stats: [55, 40, 90, 35, 50, 50],
types: ['electric']
};
pokemonCache.set('pikachu', samplePokemon, 5000); // 5 seconds
// Test immediately
const retrieved = pokemonCache.get('pikachu');
console.log('Retrieved Pokemon:', retrieved);
// Test cache stats
setTimeout(() => {
console.log('Cache stats:', pokemonCache.getStats());
}, 1000);
}
testUniversalCache();
Your task:
What you'll do: Integrate caching with real API calls Time: 20 minutes
async function fetchPokemonWithCache(pokemonName) {
const cacheKey = pokemonName.toLowerCase();
// Try to get from cache first
const cachedData = pokemonCache.get(cacheKey);
if (cachedData) {
displayPokemonData(cachedData, true); // true = from cache
return cachedData;
}
// If not in cache, fetch from API
try {
showLoadingState('pokemon');
console.log(`🌐 Fetching fresh Pokemon data: ${pokemonName}`);
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonName.toLowerCase()}`);
if (!response.ok) {
throw new Error(`Pokemon ${pokemonName} not found`);
}
const pokemonData = await response.json();
// Process and cache the data (cache for 30 minutes)
const processedData = {
name: pokemonData.name,
id: pokemonData.id,
stats: pokemonData.stats.map(s => s.base_stat),
statNames: pokemonData.stats.map(s => s.stat.name),
types: pokemonData.types.map(t => t.type.name),
sprite: pokemonData.sprites.front_default,
height: pokemonData.height,
weight: pokemonData.weight
};
pokemonCache.set(cacheKey, processedData, 30 * 60 * 1000);
displayPokemonData(processedData, false); // false = fresh from API
hideLoadingState('pokemon');
return processedData;
} catch (error) {
console.error('Failed to fetch Pokemon:', error);
hideLoadingState('pokemon');
showErrorState('pokemon', error.message);
}
}
function displayPokemonData(data, isFromCache) {
const statusElement = document.getElementById('dataStatus');
const pokemonContainer = document.getElementById('pokemonDisplay');
if (isFromCache) {
statusElement.textContent = '⚡ Loaded from cache (fast!)';
statusElement.className = 'status cached';
} else {
statusElement.textContent = '🌐 Fresh data from API';
statusElement.className = 'status fresh';
}
pokemonContainer.innerHTML = `
<div class="pokemon-card">
<h2>${data.name.charAt(0).toUpperCase() + data.name.slice(1)}</h2>
<img src="${data.sprite}" alt="${data.name}">
<div class="types">Types: ${data.types.join(', ')}</div>
<div class="stats">
${data.statNames.map((name, i) =>
`<div>${name}: ${data.stats[i]}</div>`
).join('')}
</div>
</div>
`;
}
async function fetchCharacterWithCache(characterId) {
const cacheKey = `character_${characterId}`;
const cachedData = characterCache.get(cacheKey);
if (cachedData) {
displayCharacterData(cachedData, true);
return cachedData;
}
try {
showLoadingState('character');
const response = await fetch(`https://swapi.dev/api/people/${characterId}/`);
if (!response.ok) throw new Error('Character not found');
const characterData = await response.json();
// Process and cache for 1 hour
const processedData = {
name: characterData.name,
height: characterData.height,
mass: characterData.mass,
birth_year: characterData.birth_year,
eye_color: characterData.eye_color,
films: characterData.films.length,
homeworld: characterData.homeworld
};
characterCache.set(cacheKey, processedData, 60 * 60 * 1000);
displayCharacterData(processedData, false);
hideLoadingState('character');
return processedData;
} catch (error) {
console.error('Failed to fetch character:', error);
hideLoadingState('character');
showErrorState('character', error.message);
}
}
async function fetchWeatherWithCache(city) {
const cacheKey = city.toLowerCase().replace(/\s+/g, '_');
const cachedData = weatherCache.get(cacheKey);
if (cachedData) {
displayWeatherData(cachedData, true);
return cachedData;
}
try {
showLoadingState('weather');
const apiKey = 'your-api-key-here';
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`
);
if (!response.ok) throw new Error('Weather data not found');
const weatherData = await response.json();
// Cache for 10 minutes (weather changes frequently)
weatherCache.set(cacheKey, weatherData, 10 * 60 * 1000);
displayWeatherData(weatherData, false);
hideLoadingState('weather');
return weatherData;
} catch (error) {
console.error('Failed to fetch weather:', error);
hideLoadingState('weather');
showErrorState('weather', error.message);
}
}
Your task:
What you'll do: Implement intelligent caching patterns Time: 15 minutes
class SmartCacheManager {
constructor() {
this.strategies = {
pokemon: {
expiry: 24 * 60 * 60 * 1000, // 24 hours (static data)
priority: 'high',
preload: true
},
character: {
expiry: 24 * 60 * 60 * 1000, // 24 hours (static data)
priority: 'high',
preload: false
},
weather: {
expiry: 10 * 60 * 1000, // 10 minutes (dynamic data)
priority: 'medium',
preload: false
},
country: {
expiry: 7 * 24 * 60 * 60 * 1000, // 1 week (very static)
priority: 'low',
preload: false
}
};
}
getOptimalCacheTime(dataType) {
return this.strategies[dataType]?.expiry || 10 * 60 * 1000;
}
shouldPreload(dataType) {
return this.strategies[dataType]?.preload || false;
}
async preloadPopularData() {
console.log('🔄 Preloading popular data...');
// Preload popular Pokemon
if (this.shouldPreload('pokemon')) {
const popularPokemon = ['pikachu', 'charizard', 'blastoise', 'venusaur'];
for (const pokemon of popularPokemon) {
try {
await fetchPokemonWithCache(pokemon);
await new Promise(resolve => setTimeout(resolve, 500)); // Rate limiting
} catch (error) {
console.log(`Failed to preload ${pokemon}`);
}
}
}
console.log('✅ Preloading complete');
}
// Background refresh for data nearing expiration
backgroundRefresh() {
const caches = [pokemonCache, characterCache, weatherCache, countryCache];
caches.forEach(cache => {
Object.keys(localStorage)
.filter(key => key.startsWith(cache.cachePrefix))
.forEach(async (key) => {
try {
const cacheData = JSON.parse(localStorage.getItem(key));
const timeLeft = cacheData.expiry - Date.now();
// Refresh if expiring within 2 minutes
if (timeLeft < 2 * 60 * 1000 && timeLeft > 0) {
const itemKey = key.replace(cache.cachePrefix, '');
console.log(`🔄 Background refresh for ${itemKey}`);
// Trigger refresh based on data type
if (key.includes('pokemon_')) {
await fetchPokemonWithCache(itemKey);
} else if (key.includes('character_')) {
await fetchCharacterWithCache(itemKey.replace('character_', ''));
} else if (key.includes('weather_')) {
await fetchWeatherWithCache(itemKey.replace(/_/g, ' '));
}
}
} catch (error) {
console.log('Background refresh failed:', error);
}
});
});
}
}
const smartCache = new SmartCacheManager();
// Run background refresh every 5 minutes
setInterval(() => smartCache.backgroundRefresh(), 5 * 60 * 1000);
// Preload on app start
smartCache.preloadPopularData();
class PriorityCache extends UniversalCache {
constructor(cachePrefix) {
super(cachePrefix);
this.priorities = new Map();
}
set(key, data, expiryTime, priority = 'medium') {
this.priorities.set(key, {
priority: priority,
accessCount: 0,
lastAccess: Date.now()
});
return super.set(key, data, expiryTime);
}
get(key) {
const data = super.get(key);
if (data && this.priorities.has(key)) {
const priorityData = this.priorities.get(key);
priorityData.accessCount++;
priorityData.lastAccess = Date.now();
this.priorities.set(key, priorityData);
}
return data;
}
// Clear cache intelligently based on priority
smartClear() {
const items = Array.from(this.priorities.entries())
.map(([key, data]) => ({ key, ...data }))
.sort((a, b) => {
// Sort by priority, then by access count, then by last access
const priorityOrder = { 'low': 1, 'medium': 2, 'high': 3 };
if (priorityOrder[a.priority] !== priorityOrder[b.priority]) {
return priorityOrder[a.priority] - priorityOrder[b.priority];
}
if (a.accessCount !== b.accessCount) {
return a.accessCount - b.accessCount;
}
return a.lastAccess - b.lastAccess;
});
// Remove lowest priority items first
const itemsToRemove = items.slice(0, Math.ceil(items.length * 0.3));
itemsToRemove.forEach(item => {
this.remove(item.key);
this.priorities.delete(item.key);
});
console.log(`🧹 Smart cleared ${itemsToRemove.length} items`);
}
}
// Upgrade to priority cache
const priorityPokemonCache = new PriorityCache('pokemon_');
Your task:
What you'll do: Handle offline scenarios elegantly Time: 10 minutes
class OfflineManager {
constructor() {
this.isOnline = navigator.onLine;
this.setupEventListeners();
this.initializeOfflineIndicator();
}
setupEventListeners() {
window.addEventListener('online', () => {
this.isOnline = true;
console.log('🌐 Back online!');
this.updateOfflineIndicator(false);
this.syncWhenOnline();
});
window.addEventListener('offline', () => {
this.isOnline = false;
console.log('📵 Gone offline');
this.updateOfflineIndicator(true);
});
}
initializeOfflineIndicator() {
const indicator = document.getElementById('offlineIndicator');
if (indicator) {
this.updateOfflineIndicator(!this.isOnline);
}
}
updateOfflineIndicator(isOffline) {
const indicator = document.getElementById('offlineIndicator');
if (indicator) {
indicator.style.display = isOffline ? 'block' : 'none';
indicator.textContent = isOffline ?
'📵 Offline - Using cached data' :
'🌐 Online';
}
}
async syncWhenOnline() {
if (!this.isOnline) return;
console.log('🔄 Syncing data now that we\'re online...');
// Refresh any stale cache entries
smartCache.backgroundRefresh();
// Update any pending offline changes
this.processPendingUpdates();
}
processPendingUpdates() {
const pendingUpdates = JSON.parse(localStorage.getItem('pendingUpdates') || '[]');
pendingUpdates.forEach(async (update) => {
try {
// Process each pending update
console.log('Processing pending update:', update);
// Your update logic here
} catch (error) {
console.error('Failed to process pending update:', error);
}
});
// Clear pending updates
localStorage.removeItem('pendingUpdates');
}
// Enhanced fetch with offline support
async fetchWithOfflineSupport(url, cacheKey, cache, fetchFunction) {
if (!this.isOnline) {
console.log('📵 Offline - using cache only');
const cachedData = cache.get(cacheKey);
if (cachedData) {
return { data: cachedData, fromCache: true, offline: true };
} else {
throw new Error('No internet connection and no cached data available');
}
}
// Try online fetch with cache fallback
try {
return await fetchFunction();
} catch (error) {
console.log('🔄 API failed, trying cache fallback...');
const cachedData = cache.get(cacheKey);
if (cachedData) {
return { data: cachedData, fromCache: true, apiError: true };
}
throw error;
}
}
}
// Initialize offline manager
const offlineManager = new OfflineManager();
// Enhanced Pokemon fetch with offline support
async function fetchPokemonOfflineReady(pokemonName) {
const cacheKey = pokemonName.toLowerCase();
try {
const result = await offlineManager.fetchWithOfflineSupport(
`https://pokeapi.co/api/v2/pokemon/${pokemonName}`,
cacheKey,
pokemonCache,
() => fetchPokemonWithCache(pokemonName)
);
// Display with appropriate status
if (result.offline) {
displayPokemonData(result.data, true, '📵 Offline mode');
} else if (result.apiError) {
displayPokemonData(result.data, true, '⚠️ Using cached data (API unavailable)');
} else {
displayPokemonData(result.data, result.fromCache);
}
return result.data;
} catch (error) {
showErrorState('pokemon', 'No data available offline');
throw error;
}
}
Your task:
Try these scenarios:
Easy: Add cache size monitoring and warnings
Medium: Implement cache compression for large datasets
Hard: Create cache synchronization across browser tabs
Excellent work on building a comprehensive caching system! You've learned how to optimize API performance and create offline-capable applications. In the next lesson, we'll explore advanced error handling and retry logic to make your API integrations even more robust.
Cache not working?
Performance issues?
cache.getStats()
Offline mode not working?