javascript

exercises

exercises.js
/**
 * ========================================
 * 11.3 Fetch API and HTTP - Exercises
 * ========================================
 *
 * Practice making HTTP requests with the Fetch API.
 * Complete each exercise by filling in the code.
 */

/**
 * EXERCISE 1: Basic GET Request
 *
 * Fetch data from a URL and return parsed JSON.
 */

async function fetchData(url) {
  // Fetch from URL
  // Return parsed JSON data
  // Throw error with status code if not ok
}

// Test with:
// fetchData('https://jsonplaceholder.typicode.com/posts/1')

/**
 * EXERCISE 2: POST Request
 *
 * Send JSON data to an endpoint.
 */

async function postData(url, data) {
  // Send POST request with JSON body
  // Include proper Content-Type header
  // Return response JSON
}

// Test:
// postData('https://jsonplaceholder.typicode.com/posts', {
//     title: 'Test',
//     body: 'Content'
// })

/**
 * EXERCISE 3: Query Parameters
 *
 * Fetch with URL query parameters.
 */

async function fetchWithQuery(baseUrl, params) {
  // Build URL with query parameters
  // Fetch and return JSON
}

// Test:
// fetchWithQuery('https://jsonplaceholder.typicode.com/posts', {
//     userId: 1,
//     _limit: 5
// })

/**
 * EXERCISE 4: Error Handler
 *
 * Create a fetch wrapper with comprehensive error handling.
 */

async function safeFetch(url, options = {}) {
  // Handle network errors
  // Handle HTTP errors (4xx, 5xx)
  // Return { success: true, data } or { success: false, error, status? }
}

// Test:
// safeFetch('https://httpstat.us/404')
// → { success: false, error: 'Not Found', status: 404 }

/**
 * EXERCISE 5: Timeout
 *
 * Fetch with automatic timeout.
 */

async function fetchWithTimeout(url, timeoutMs = 5000) {
  // Cancel request if it takes longer than timeout
  // Throw descriptive error on timeout
}

// Test:
// fetchWithTimeout('https://httpstat.us/200?sleep=10000', 2000)
// → throws timeout error

/**
 * EXERCISE 6: Retry Logic
 *
 * Retry failed requests with exponential backoff.
 */

async function fetchWithRetry(url, options = {}) {
  // options: { retries: 3, delay: 1000, backoffFactor: 2 }
  // Retry on network errors and 5xx status codes
  // Exponential backoff between retries
}

// Test:
// fetchWithRetry('https://httpstat.us/503', { retries: 3 })

/**
 * EXERCISE 7: Parallel Fetcher
 *
 * Fetch multiple URLs in parallel.
 */

async function fetchMany(urls) {
  // Fetch all URLs in parallel
  // Return array of { url, success, data?, error? }
}

// Test:
// fetchMany([
//     'https://jsonplaceholder.typicode.com/posts/1',
//     'https://jsonplaceholder.typicode.com/posts/999999',
//     'https://jsonplaceholder.typicode.com/posts/2'
// ])

/**
 * EXERCISE 8: Sequential Fetcher
 *
 * Fetch URLs one after another.
 */

async function fetchSequence(urls) {
  // Fetch URLs sequentially (wait for each before starting next)
  // Return array of results
}

/**
 * EXERCISE 9: Request with Auth
 *
 * Create authenticated request helper.
 */

function createAuthenticatedFetch(token) {
  // Return a fetch function that adds Bearer token to all requests
  // Should work like: authFetch(url, options)
}

// Test:
// const authFetch = createAuthenticatedFetch('my-secret-token');
// authFetch('/api/data')

/**
 * EXERCISE 10: Progress Tracker
 *
 * Download with progress tracking.
 */

async function downloadWithProgress(url, onProgress) {
  // Download file and report progress
  // onProgress(percent, loaded, total)
  // Return blob
}

/**
 * EXERCISE 11: File Uploader
 *
 * Upload files using FormData.
 */

async function uploadFiles(url, files, metadata = {}) {
  // files: FileList or array of Files
  // metadata: additional form fields
  // Return server response
}

/**
 * EXERCISE 12: Cancellable Request
 *
 * Create request that can be cancelled.
 */

function createCancellableRequest(url, options = {}) {
  // Return { promise, cancel }
  // cancel() should abort the request
  // Promise should reject with 'Request cancelled' on cancel
}

/**
 * EXERCISE 13: Rate Limiter
 *
 * Limit requests to a certain rate.
 */

function createRateLimiter(requestsPerSecond = 5) {
  // Return async function that rate-limits fetch calls
  // Queue requests if limit exceeded
}

// Test:
// const limitedFetch = createRateLimiter(2);
// for (let i = 0; i < 10; i++) {
//     limitedFetch(`/api/item/${i}`);
// }

/**
 * EXERCISE 14: Response Cache
 *
 * Cache responses with TTL.
 */

class FetchCache {
  constructor(ttlMs = 60000) {
    // Initialize cache with time-to-live
  }

  async fetch(url, options = {}) {
    // Return cached response if valid
    // Otherwise fetch, cache, and return
  }

  invalidate(url) {
    // Remove URL from cache
  }

  clear() {
    // Clear entire cache
  }
}

/**
 * EXERCISE 15: API Client
 *
 * Full-featured API client.
 */

class APIClient {
  constructor(baseUrl, defaultHeaders = {}) {
    // Initialize with base URL and default headers
  }

  setHeader(name, value) {
    // Set or update a default header
  }

  async get(endpoint, params = {}) {
    // GET request with query params
  }

  async post(endpoint, data) {
    // POST with JSON body
  }

  async put(endpoint, data) {
    // PUT with JSON body
  }

  async patch(endpoint, data) {
    // PATCH with JSON body
  }

  async delete(endpoint) {
    // DELETE request
  }
}

// ============================================
// SOLUTIONS (Hidden - Scroll to reveal)
// ============================================

/*
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 * SOLUTIONS BELOW
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

// SOLUTION 1: Basic GET Request
async function fetchDataSolution(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP Error: ${response.status}`);
  }

  return response.json();
}

// SOLUTION 2: POST Request
async function postDataSolution(url, data) {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  });

  if (!response.ok) {
    throw new Error(`HTTP Error: ${response.status}`);
  }

  return response.json();
}

// SOLUTION 3: Query Parameters
async function fetchWithQuerySolution(baseUrl, params) {
  const url = new URL(baseUrl);

  Object.entries(params).forEach(([key, value]) => {
    if (value !== undefined && value !== null) {
      url.searchParams.append(key, value);
    }
  });

  const response = await fetch(url);
  return response.json();
}

// SOLUTION 4: Error Handler
async function safeFetchSolution(url, options = {}) {
  try {
    const response = await fetch(url, options);

    if (!response.ok) {
      return {
        success: false,
        error: response.statusText || 'Request failed',
        status: response.status,
      };
    }

    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    return {
      success: false,
      error: error.message || 'Network error',
    };
  }
}

// SOLUTION 5: Timeout
async function fetchWithTimeoutSolution(url, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeoutMs}ms`);
    }
    throw error;
  }
}

// SOLUTION 6: Retry Logic
async function fetchWithRetrySolution(url, options = {}) {
  const { retries = 3, delay = 1000, backoffFactor = 2 } = options;
  let lastError;

  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      const response = await fetch(url);

      // Retry on 5xx errors
      if (response.status >= 500) {
        throw new Error(`Server error: ${response.status}`);
      }

      return response;
    } catch (error) {
      lastError = error;

      if (attempt < retries - 1) {
        const waitTime = delay * Math.pow(backoffFactor, attempt);
        await new Promise((r) => setTimeout(r, waitTime));
      }
    }
  }

  throw new Error(`Failed after ${retries} attempts: ${lastError.message}`);
}

// SOLUTION 7: Parallel Fetcher
async function fetchManySolution(urls) {
  const results = await Promise.allSettled(
    urls.map(async (url) => {
      const response = await fetch(url);
      if (!response.ok) throw new Error(response.statusText);
      return response.json();
    })
  );

  return urls.map((url, index) => {
    const result = results[index];
    if (result.status === 'fulfilled') {
      return { url, success: true, data: result.value };
    } else {
      return { url, success: false, error: result.reason.message };
    }
  });
}

// SOLUTION 8: Sequential Fetcher
async function fetchSequenceSolution(urls) {
  const results = [];

  for (const url of urls) {
    try {
      const response = await fetch(url);
      const data = await response.json();
      results.push({ url, success: true, data });
    } catch (error) {
      results.push({ url, success: false, error: error.message });
    }
  }

  return results;
}

// SOLUTION 9: Request with Auth
function createAuthenticatedFetchSolution(token) {
  return async function (url, options = {}) {
    return fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${token}`,
      },
    });
  };
}

// SOLUTION 10: Progress Tracker
async function downloadWithProgressSolution(url, onProgress) {
  const response = await fetch(url);
  const contentLength = response.headers.get('content-length');
  const total = parseInt(contentLength, 10) || 0;

  const reader = response.body.getReader();
  const chunks = [];
  let loaded = 0;

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    chunks.push(value);
    loaded += value.length;

    if (total && onProgress) {
      const percent = (loaded / total) * 100;
      onProgress(percent, loaded, total);
    }
  }

  return new Blob(chunks);
}

// SOLUTION 11: File Uploader
async function uploadFilesSolution(url, files, metadata = {}) {
  const formData = new FormData();

  // Add files
  const fileArray = Array.from(files);
  fileArray.forEach((file, index) => {
    formData.append(`file${index}`, file);
  });

  // Add metadata
  Object.entries(metadata).forEach(([key, value]) => {
    formData.append(key, value);
  });

  const response = await fetch(url, {
    method: 'POST',
    body: formData,
  });

  if (!response.ok) {
    throw new Error(`Upload failed: ${response.status}`);
  }

  return response.json();
}

// SOLUTION 12: Cancellable Request
function createCancellableRequestSolution(url, options = {}) {
  const controller = new AbortController();

  const promise = fetch(url, {
    ...options,
    signal: controller.signal,
  }).catch((error) => {
    if (error.name === 'AbortError') {
      throw new Error('Request cancelled');
    }
    throw error;
  });

  return {
    promise,
    cancel: () => controller.abort(),
  };
}

// SOLUTION 13: Rate Limiter
function createRateLimiterSolution(requestsPerSecond = 5) {
  const queue = [];
  const interval = 1000 / requestsPerSecond;
  let lastRequest = 0;
  let processing = false;

  async function processQueue() {
    if (processing || queue.length === 0) return;
    processing = true;

    while (queue.length > 0) {
      const now = Date.now();
      const timeSince = now - lastRequest;

      if (timeSince < interval) {
        await new Promise((r) => setTimeout(r, interval - timeSince));
      }

      const { url, options, resolve, reject } = queue.shift();
      lastRequest = Date.now();

      try {
        const response = await fetch(url, options);
        resolve(response);
      } catch (error) {
        reject(error);
      }
    }

    processing = false;
  }

  return function limitedFetch(url, options = {}) {
    return new Promise((resolve, reject) => {
      queue.push({ url, options, resolve, reject });
      processQueue();
    });
  };
}

// SOLUTION 14: Response Cache
class FetchCacheSolution {
  constructor(ttlMs = 60000) {
    this.cache = new Map();
    this.ttl = ttlMs;
  }

  getCacheKey(url, options = {}) {
    return `${options.method || 'GET'}-${url}`;
  }

  isValid(entry) {
    return Date.now() < entry.expires;
  }

  async fetch(url, options = {}) {
    const key = this.getCacheKey(url, options);
    const cached = this.cache.get(key);

    if (cached && this.isValid(cached)) {
      return cached.data;
    }

    const response = await fetch(url, options);
    const data = await response.json();

    this.cache.set(key, {
      data,
      expires: Date.now() + this.ttl,
    });

    return data;
  }

  invalidate(url, options = {}) {
    const key = this.getCacheKey(url, options);
    this.cache.delete(key);
  }

  clear() {
    this.cache.clear();
  }
}

// SOLUTION 15: API Client
class APIClientSolution {
  constructor(baseUrl, defaultHeaders = {}) {
    this.baseUrl = baseUrl;
    this.headers = {
      'Content-Type': 'application/json',
      ...defaultHeaders,
    };
  }

  setHeader(name, value) {
    this.headers[name] = value;
  }

  async request(endpoint, options = {}) {
    const url = new URL(endpoint, this.baseUrl);

    // Add query params if present
    if (options.params) {
      Object.entries(options.params).forEach(([key, value]) => {
        url.searchParams.append(key, value);
      });
    }

    const response = await fetch(url, {
      ...options,
      headers: {
        ...this.headers,
        ...options.headers,
      },
    });

    if (!response.ok) {
      const error = new Error(`HTTP ${response.status}`);
      error.status = response.status;
      throw error;
    }

    if (response.status === 204) return null;
    return response.json();
  }

  get(endpoint, params = {}) {
    return this.request(endpoint, { method: 'GET', params });
  }

  post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  patch(endpoint, data) {
    return this.request(endpoint, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
  }

  delete(endpoint) {
    return this.request(endpoint, { method: 'DELETE' });
  }
}

console.log('Fetch API exercises loaded!');
console.log(
  'Complete each exercise and check against solutions at the bottom.'
);
Exercises - JavaScript Tutorial | DeepML