Yandex Metrika
All Guides
Advanced

Advanced API Usage

Advanced techniques and optimizations with the Echoes API.

APIAdvanced

Advanced API Usage

This guide covers advanced techniques for using the Echoes API, including caching, rate limiting, and optimization strategies.

API Endpoints Overview

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

Advanced Query Techniques

You can create more complex queries with the Echoes API by combining the available parameters:

JavaScript
// 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));

Using URL Construction

For more dynamic queries, it's better to construct URLs programmatically:

JavaScript
// 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));

Pagination Strategies

When working with the full quotes collection, you should implement effective pagination:

JavaScript
// 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();
});

Caching Strategies

By caching API requests, you can improve application performance and reduce server load:

Local Storage Caching

JavaScript
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));

Service Worker Caching

For more advanced caching that works offline, you can implement a Service Worker:

JavaScript
// 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;
          });
        });
      })
    );
  }
});

Rate Limiting and Queue Management

To ensure your application remains responsive even when making multiple API calls:

JavaScript
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));

Advanced Error Handling

Comprehensive error handling and retry mechanisms for robust applications:

JavaScript
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.');
  }
}

Performance Optimizations

Batch Processing Multiple Quotes

If you need to display multiple quotes at once, you can make parallel requests:

JavaScript
// 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));

Language-Based Data Processing

When working with quotes in different languages, you might want to organize them:

JavaScript
// 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;
}

Building a Complete Quote Application

Here's a comprehensive example combining many of the techniques we've covered to create a complete quote application:

JavaScript
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.