Advanced techniques and optimizations with the Echoes API.
This guide covers advanced techniques for using the Echoes API, including caching, rate limiting, and optimization strategies.
The Echoes API provides three main endpoints:
1. GET /api/quotes - Retrieve all quotes with pagination support
2. GET /api/quotes/:id - Retrieve a specific quote by its ID
3. GET /api/quotes/random - Retrieve a random quote, with optional filtering
You can create more complex queries with the Echoes API by combining the available parameters:
// Get a random quote in Turkish by a specific author
fetch('https://echoes.soferity.com/api/quotes/random?lang=tr&author=Atatürk')
.then(response => response.json())
.then(data => console.log(data));
For more dynamic queries, it's better to construct URLs programmatically:
// Dynamically build query parameters
const url = new URL('https://echoes.soferity.com/api/quotes/random');
url.searchParams.append('lang', 'en');
url.searchParams.append('author', 'Einstein');
fetch(url)
.then(response => response.json())
.then(data => console.log(data));
When working with the full quotes collection, you should implement effective pagination:
// Fetch quotes with pagination
async function fetchQuotesPaginated(page = 1, perPage = 10) {
try {
const response = await fetch(
`https://echoes.soferity.com/api/quotes?page=${page}&perPage=${perPage}`
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error fetching quotes:', error);
throw error;
}
}
// Example usage with pagination controls
let currentPage = 1;
const itemsPerPage = 20;
async function loadCurrentPage() {
try {
const data = await fetchQuotesPaginated(currentPage, itemsPerPage);
displayQuotes(data.data);
updatePaginationControls(data.pagination);
} catch (error) {
showErrorMessage('Failed to load quotes. Please try again.');
}
}
function updatePaginationControls(pagination) {
// Update UI pagination controls
document.getElementById('current-page').textContent = pagination.page;
document.getElementById('total-pages').textContent = pagination.totalPages;
// Enable/disable previous/next buttons
document.getElementById('prev-button').disabled = pagination.page <= 1;
document.getElementById('next-button').disabled = pagination.page >= pagination.totalPages;
}
// Handle pagination button clicks
document.getElementById('prev-button').addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
loadCurrentPage();
}
});
document.getElementById('next-button').addEventListener('click', () => {
currentPage++;
loadCurrentPage();
});
By caching API requests, you can improve application performance and reduce server load:
async function getQuoteWithCache(params = {}) {
// Create a unique cache key based on request parameters
const cacheKey = `echoes_quotes_${JSON.stringify(params)}`;
const cachedData = localStorage.getItem(cacheKey);
if (cachedData) {
const { data, timestamp } = JSON.parse(cachedData);
const cacheAge = Date.now() - timestamp;
// Return from cache if it's less than 1 hour old
if (cacheAge < 3600000) {
console.log('Quote retrieved from cache');
return data;
}
}
// If not in cache or expired, fetch from API
console.log('Fetching quote from API');
try {
// Build the URL with parameters
const url = new URL('https://echoes.soferity.com/api/quotes/random');
// Add parameters to URL
Object.keys(params).forEach(key =>
url.searchParams.append(key, params[key])
);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
// Cache the data with timestamp
localStorage.setItem(cacheKey, JSON.stringify({
data,
timestamp: Date.now()
}));
return data;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
// Usage examples
getQuoteWithCache({ lang: 'en' })
.then(quote => console.log('English quote:', quote))
.catch(error => console.error('Error:', error));
getQuoteWithCache({ author: 'Einstein' })
.then(quote => console.log('Einstein quote:', quote))
.catch(error => console.error('Error:', error));
For more advanced caching that works offline, you can implement a Service Worker:
// In service-worker.js
self.addEventListener('fetch', (event) => {
// Only cache Echoes API requests
if (event.request.url.includes('echoes.soferity.com/api')) {
event.respondWith(
caches.open('echoes-api-cache').then((cache) => {
return cache.match(event.request).then((response) => {
// If in cache and less than 1 hour old, return from cache
if (response) {
const fetchDate = response.headers.get('fetch-date');
const age = Date.now() - new Date(fetchDate).getTime();
if (age < 3600000) { // 1 hour (milliseconds)
return response;
}
}
// If not in cache or expired, fetch from network
return fetch(event.request).then((networkResponse) => {
// Create a copy of the response (since the original is a stream)
const responseToCache = networkResponse.clone();
// Add a custom header with timestamp
const headers = new Headers(responseToCache.headers);
headers.append('fetch-date', new Date().toISOString());
// Create response with new headers
const responseWithTime = new Response(
responseToCache.body,
{
status: responseToCache.status,
statusText: responseToCache.statusText,
headers: headers
}
);
// Cache it
cache.put(event.request, responseWithTime);
return networkResponse;
});
});
})
);
}
});
To ensure your application remains responsive even when making multiple API calls:
class EchoesClient {
constructor() {
this.baseUrl = 'https://echoes.soferity.com/api';
this.requestQueue = [];
this.isProcessing = false;
this.requestsPerMinute = 60; // Maximum of 60 requests per minute
this.requestTimes = [];
}
async request(endpoint, params = {}) {
return new Promise((resolve, reject) => {
// Add request to queue
this.requestQueue.push({
endpoint,
params,
resolve,
reject
});
// Start queue processor (if not already running)
if (!this.isProcessing) {
this.processQueue();
}
});
}
async processQueue() {
if (this.requestQueue.length === 0) {
this.isProcessing = false;
return;
}
this.isProcessing = true;
// Check rate limit compliance
if (this.requestTimes.length >= this.requestsPerMinute) {
// Find the oldest request within the last minute
const oldestRequest = this.requestTimes[0];
const timeSinceOldest = Date.now() - oldestRequest;
// If less than a minute has passed, wait
if (timeSinceOldest < 60000) {
const waitTime = 60000 - timeSinceOldest;
console.log(`Rate limit protection: waiting ${waitTime}ms`);
await new Promise(resolve => setTimeout(resolve, waitTime));
// Remove oldest time as time has passed
this.requestTimes.shift();
} else {
// If more than a minute has passed, clean up old times
this.requestTimes = this.requestTimes.filter(time => (Date.now() - time) < 60000);
}
}
// Get next request from queue
const { endpoint, params, resolve, reject } = this.requestQueue.shift();
// Record time of this request
this.requestTimes.push(Date.now());
try {
// Build URL parameters
const url = new URL(`${this.baseUrl}${endpoint}`);
Object.keys(params).forEach(key =>
url.searchParams.append(key, params[key])
);
const response = await fetch(url);
// Check HTTP status codes
if (response.ok) {
const data = await response.json();
resolve(data);
} else {
throw new Error(`HTTP error: ${response.status}`);
}
} catch (error) {
reject(error);
}
// Process more requests if there are any in the queue
setTimeout(() => this.processQueue(), 0);
}
// Helper methods for specific endpoints
async getRandomQuote(params = {}) {
return this.request('/quotes/random', params);
}
async getAllQuotes(page = 1, perPage = 10) {
return this.request('/quotes', { page, perPage });
}
async getQuoteById(id) {
return this.request(`/quotes/${id}`);
}
}
// Usage example
const echoesClient = new EchoesClient();
// Get a random quote
echoesClient.getRandomQuote({ lang: 'tr' })
.then(quote => console.log('Random quote:', quote))
.catch(err => console.error('Error:', err));
// Get quotes with pagination
echoesClient.getAllQuotes(2, 15)
.then(data => console.log('Page 2 quotes:', data))
.catch(err => console.error('Error:', err));
Comprehensive error handling and retry mechanisms for robust applications:
async function fetchWithAdvancedErrorHandling(url, options = {}, maxRetries = 3) {
let lastError;
let retryCount = 0;
while (retryCount < maxRetries) {
try {
const response = await fetch(url, options);
// Check HTTP status codes
if (response.ok) {
return await response.json();
}
// Handle different error scenarios
switch (response.status) {
case 400: // Bad Request
throw new Error('Invalid request parameters. Please check your request.');
case 404: // Not Found
throw new Error('The requested quote or resource was not found.');
case 429: // Too Many Requests (if API implements rate limiting)
const retryAfter = response.headers.get('Retry-After') || 2 ** retryCount;
console.log(`Rate limit exceeded. Waiting ${retryAfter} seconds...`);
await new Promise(r => setTimeout(r, retryAfter * 1000));
retryCount++;
continue;
case 500: // Server Error
case 502: // Bad Gateway
case 503: // Service Unavailable
case 504: // Gateway Timeout
// Retry server errors with exponential backoff
const backoffTime = Math.min(1000 * (2 ** retryCount), 30000);
console.log(`Server error. Retrying in ${backoffTime/1000} seconds...`);
await new Promise(r => setTimeout(r, backoffTime));
retryCount++;
continue;
default:
throw new Error(`HTTP error: ${response.status}`);
}
} catch (error) {
lastError = error;
// Retry network errors (internet connectivity issues)
if (error.name === 'TypeError' && error.message.includes('fetch')) {
const backoffTime = Math.min(1000 * (2 ** retryCount), 30000);
console.log(`Network error. Retrying in ${backoffTime/1000} seconds...`);
await new Promise(r => setTimeout(r, backoffTime));
retryCount++;
} else {
// Don't retry for other errors
throw error;
}
}
}
// When all retries fail
throw new Error(`Maximum retries reached. Last error: ${lastError.message}`);
}
// Usage example
fetchWithAdvancedErrorHandling('https://echoes.soferity.com/api/quotes/random?lang=tr')
.then(data => console.log('Quote:', data))
.catch(error => {
console.error('Error:', error.message);
// Show a user-friendly error message
showUserFriendlyError(error);
});
function showUserFriendlyError(error) {
// Different feedback for the user based on the error message
if (error.message.includes('Invalid request')) {
alert('There is an issue with your request parameters. Please check your request.');
} else if (error.message.includes('rate limit')) {
alert('You have sent too many requests. Please wait and try again.');
} else if (error.message.includes('not found')) {
alert('The quote you are looking for was not found. Please try a different request.');
} else if (error.message.includes('Server error')) {
alert('The server is not responding. Please try again later.');
} else {
alert('An issue occurred. Please try again later.');
}
}
If you need to display multiple quotes at once, you can make parallel requests:
// Fetch multiple random quotes in parallel
async function fetchMultipleRandomQuotes(count, params = {}) {
try {
// Create an array of promises for each request
const promises = Array(count).fill().map(() => {
// Build the URL with parameters
const url = new URL('https://echoes.soferity.com/api/quotes/random');
// Add parameters to URL
Object.keys(params).forEach(key =>
url.searchParams.append(key, params[key])
);
// Return the fetch promise
return fetch(url.toString())
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
});
});
// Wait for all promises to resolve
return await Promise.all(promises);
} catch (error) {
console.error('Error fetching multiple quotes:', error);
throw error;
}
}
// Usage example: get 3 quotes by Einstein
fetchMultipleRandomQuotes(3, { author: 'Einstein' })
.then(quotes => {
console.log('Multiple Einstein quotes:');
quotes.forEach((quote, index) => {
console.log(`Quote ${index + 1}:`, quote);
});
})
.catch(error => console.error('Error:', error));
When working with quotes in different languages, you might want to organize them:
// Process quotes by language
function organizeQuotesByLanguage(quotes) {
const quotesByLanguage = {};
quotes.forEach(quote => {
const lang = quote.lang || 'unknown';
// Initialize the language array if it doesn't exist
if (!quotesByLanguage[lang]) {
quotesByLanguage[lang] = [];
}
// Add the quote to its language category
quotesByLanguage[lang].push({
id: quote.id,
text: quote.quote,
author: quote.author,
// Add a formatted display version
displayText: `"${quote.quote}" — ${quote.author}`
});
});
return quotesByLanguage;
}
// Usage example
fetchMultipleRandomQuotes(10)
.then(quotes => {
const organizedQuotes = organizeQuotesByLanguage(quotes);
console.log('Quotes by language:', organizedQuotes);
// Now you can easily display quotes by language
Object.keys(organizedQuotes).forEach(lang => {
console.log(`${getLanguageName(lang)} quotes: ${organizedQuotes[lang].length}`);
});
})
.catch(error => console.error('Error:', error));
// Helper function to get language name
function getLanguageName(langCode) {
const languages = {
'en': 'English',
'tr': 'Turkish',
'es': 'Spanish',
'fr': 'French',
'de': 'German'
// Add other languages as needed
};
return languages[langCode] || langCode;
}
Here's a comprehensive example combining many of the techniques we've covered to create a complete quote application:
class QuoteManager {
constructor(options = {}) {
this.baseUrl = options.baseUrl || 'https://echoes.soferity.com/api';
this.defaultLang = options.defaultLang || 'en';
this.cacheEnabled = options.cacheEnabled !== false;
this.cacheTime = options.cacheTime || 3600; // seconds
// Initialize cache
this.cache = new Map();
// Set up cache cleanup interval
if (this.cacheEnabled) {
this.cacheCleanupInterval = setInterval(() => {
this.cleanCache();
}, 60000); // Clean every minute
}
// Keep track of favorite quotes
this.favorites = this.loadFavorites();
}
// Core API request method with caching
async fetchQuote(endpoint, params = {}) {
// Create cache key
const cacheKey = `${endpoint}:${JSON.stringify(params)}`;
// Check cache first
if (this.cacheEnabled) {
const cachedData = this.getFromCache(cacheKey);
if (cachedData) {
return cachedData;
}
}
try {
// Build URL with parameters
const url = new URL(`${this.baseUrl}${endpoint}`);
Object.keys(params).forEach(key => {
if (params[key] !== undefined && params[key] !== null) {
url.searchParams.append(key, params[key]);
}
});
// Make the request
const response = await fetch(url);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
// Cache the result
if (this.cacheEnabled) {
this.saveToCache(cacheKey, data);
}
return data;
} catch (error) {
console.error('Quote fetch error:', error);
throw error;
}
}
// Get a random quote
async getRandomQuote(params = {}) {
// Add default language if not specified
if (!params.lang) {
params.lang = this.defaultLang;
}
return this.fetchQuote('/quotes/random', params);
}
// Get a specific quote by ID
async getQuoteById(id) {
return this.fetchQuote(`/quotes/${id}`);
}
// Get quotes with pagination
async getQuotes(page = 1, perPage = 10, params = {}) {
return this.fetchQuote('/quotes', {
page,
perPage,
...params
});
}
// Favorites management
addToFavorites(quote) {
if (!this.favorites.find(fav => fav.id === quote.id)) {
this.favorites.push(quote);
this.saveFavorites();
return true;
}
return false;
}
removeFromFavorites(quoteId) {
const initialLength = this.favorites.length;
this.favorites = this.favorites.filter(quote => quote.id !== quoteId);
if (initialLength !== this.favorites.length) {
this.saveFavorites();
return true;
}
return false;
}
getFavorites() {
return [...this.favorites];
}
loadFavorites() {
try {
const saved = localStorage.getItem('quote_favorites');
return saved ? JSON.parse(saved) : [];
} catch (error) {
console.error('Error loading favorites:', error);
return [];
}
}
saveFavorites() {
try {
localStorage.setItem('quote_favorites', JSON.stringify(this.favorites));
} catch (error) {
console.error('Error saving favorites:', error);
}
}
// Cache management
getFromCache(key) {
if (!this.cache.has(key)) {
return null;
}
const cacheEntry = this.cache.get(key);
const now = Date.now();
// Return null if cache has expired
if (now - cacheEntry.timestamp > this.cacheTime * 1000) {
this.cache.delete(key);
return null;
}
return cacheEntry.data;
}
saveToCache(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
cleanCache() {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > this.cacheTime * 1000) {
this.cache.delete(key);
}
}
}
// Clean up resources
destroy() {
if (this.cacheCleanupInterval) {
clearInterval(this.cacheCleanupInterval);
}
}
}
// Usage example
const quoteManager = new QuoteManager({
defaultLang: 'en',
cacheEnabled: true,
cacheTime: 1800 // 30 minutes
});
// UI Integration Example
async function initializeQuoteApp() {
const quoteContainer = document.getElementById('quote-container');
const nextButton = document.getElementById('next-quote');
const favButton = document.getElementById('favorite-quote');
const langSelector = document.getElementById('language-selector');
const authorInput = document.getElementById('author-input');
const searchButton = document.getElementById('search-button');
const favoritesList = document.getElementById('favorites-list');
let currentQuote = null;
// Display a quote in the container
function displayQuote(quote) {
currentQuote = quote;
quoteContainer.innerHTML = `
<blockquote class="quote-text">${quote.quote}</blockquote>
<cite class="quote-author">— ${quote.author}</cite>
<div class="quote-meta">
<span class="quote-language">${getLanguageName(quote.lang)}</span>
<span class="quote-id">ID: ${quote.id}</span>
</div>
`;
// Update favorite button state
const isFavorite = quoteManager.getFavorites().some(fav => fav.id === quote.id);
favButton.textContent = isFavorite ? '★ Remove from Favorites' : '☆ Add to Favorites';
favButton.className = isFavorite ? 'btn-favorite active' : 'btn-favorite';
}
// Load and display a random quote
async function loadRandomQuote() {
try {
quoteContainer.innerHTML = '<div class="loading">Loading...</div>';
const params = {
lang: langSelector.value || undefined,
author: authorInput.value || undefined
};
const quote = await quoteManager.getRandomQuote(params);
displayQuote(quote);
} catch (error) {
quoteContainer.innerHTML = `
<div class="error">
Failed to load quote. Please try again.
<p>${error.message}</p>
</div>
`;
}
}
// Toggle favorite status of current quote
function toggleFavorite() {
if (!currentQuote) return;
const isFavorite = quoteManager.getFavorites().some(fav => fav.id === currentQuote.id);
if (isFavorite) {
quoteManager.removeFromFavorites(currentQuote.id);
favButton.textContent = '☆ Add to Favorites';
favButton.className = 'btn-favorite';
} else {
quoteManager.addToFavorites(currentQuote);
favButton.textContent = '★ Remove from Favorites';
favButton.className = 'btn-favorite active';
}
// Update favorites list
updateFavoritesList();
}
// Update the favorites list in the UI
function updateFavoritesList() {
const favorites = quoteManager.getFavorites();
if (favorites.length === 0) {
favoritesList.innerHTML = '<p class="empty-list">No favorite quotes yet.</p>';
return;
}
favoritesList.innerHTML = favorites.map(quote => `
<div class="favorite-item" data-id="${quote.id}">
<blockquote>${quote.quote}</blockquote>
<cite>— ${quote.author}</cite>
<button class="remove-favorite" data-id="${quote.id}">Remove</button>
</div>
`).join('');
// Add event listeners to remove buttons
document.querySelectorAll('.remove-favorite').forEach(button => {
button.addEventListener('click', (e) => {
const id = parseInt(e.target.dataset.id);
quoteManager.removeFromFavorites(id);
updateFavoritesList();
// Update current quote display if it's the same one
if (currentQuote && currentQuote.id === id) {
favButton.textContent = '☆ Add to Favorites';
favButton.className = 'btn-favorite';
}
});
});
}
// Set up event listeners
nextButton.addEventListener('click', loadRandomQuote);
favButton.addEventListener('click', toggleFavorite);
searchButton.addEventListener('click', loadRandomQuote);
// Initial setup
updateFavoritesList();
await loadRandomQuote();
}
// Helper function to get language name from code
function getLanguageName(langCode) {
const languages = {
'en': 'English',
'tr': 'Turkish',
'es': 'Spanish',
'fr': 'French',
'de': 'German'
// Add other languages as needed
};
return languages[langCode] || langCode;
}
// Initialize the app when DOM is ready
document.addEventListener('DOMContentLoaded', initializeQuoteApp);
This comprehensive example demonstrates how to build a complete quote application with the Echoes API, featuring:
- Efficient API requests with caching
- Favorites management with local storage
- Language filtering
- Author filtering
- Error handling
- A responsive user interface
By implementing these advanced techniques, you can create robust, efficient applications that make the most of the Echoes API's capabilities.