Courses/JavaScript/Building Custom APIs
    Back to Course

    🎯 What You'll Learn

    • Creating reusable fetch wrappers
    • Timeout & AbortController patterns
    • Retry with exponential backoff
    • Auth token injection
    • Response caching & deduplication
    • Building an OOP API client

    đź’ˇ Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

    • Download Node.js to run JavaScript on your computer
    • Use your browser's Developer Console (Press F12) to test code snippets
    • Create a .html file with <script> tags and open it in your browser

    Building Custom APIs with Fetch & Async Logic

    Master the art of designing clean, reusable API layers using fetch, async/await, and production-grade patterns used by professional developers.

    Understanding the Role of an API Layer

    An API layer acts as the "communication brain" of your application. Instead of writing dozens of repetitive fetch calls scattered throughout your codebase, you centralize everything into a reusable system. This architecture is used in frameworks like Axios, React Query, tRPC, and enterprise-grade SDKs.

    A well-designed API layer helps with:

    • Reducing duplicated code — write fetch logic once
    • Adding global headers — auth tokens, content types
    • Handling JSON parsing safely — prevent crashes
    • Managing errors centrally — consistent error messages
    • Retrying failed requests — improve reliability
    • Aborting slow requests — prevent UI hangs
    • Caching responses — boost performance

    Creating a Base Fetch Wrapper

    Let's start with a base utility that handles automatic JSON conversion, standardized error messages, status code interpretation, and default headers. This eliminates 90% of the boilerplate beginners write.

    Base Fetch Wrapper

    Create a reusable API request utility

    Try it Yourself »
    JavaScript
    export async function apiRequest(url, options = {}) {
      try {
        const response = await fetch(url, {
          headers: {
            "Content-Type": "application/json",
            ...(options.headers || {})
          },
          ...options
        });
    
        const contentType = response.headers.get("Content-Type");
        const body = contentType?.includes("application/json")
          ? await response.json()
          : await response.text();
    
        if (!response.ok) {
          throw new Error(
            `Request failed (${response.s
    ...

    Adding Timeout Support (AbortController)

    Sometimes APIs freeze or respond too slowly, causing the UI to hang. The AbortController API lets you cancel requests that take too long. This is essential for production apps where network conditions are unpredictable.

    Timeout Support

    Implement request timeout with AbortController

    Try it Yourself »
    JavaScript
    function fetchWithTimeout(url, ms = 7000) {
      const controller = new AbortController();
      const id = setTimeout(() => controller.abort(), ms);
    
      return fetch(url, { signal: controller.signal }).finally(() =>
        clearTimeout(id)
      );
    }
    
    // Usage - abort after 3 seconds
    fetchWithTimeout("https://jsonplaceholder.typicode.com/users", 3000)
      .then(res => res.json())
      .then(data => console.log("Loaded:", data.length, "users"))
      .catch(err => {
        if (err.name === "AbortError") {
          console.lo
    ...

    Adding Automatic Retries with Exponential Backoff

    Networks fail—especially on mobile. Retries with exponential backoff dramatically improve reliability. This technique is used in Stripe, Shopify, Google APIs, and most major SDKs. The delay doubles after each failed attempt (300ms → 600ms → 1200ms).

    Automatic Retries

    Implement exponential backoff retry logic

    Try it Yourself »
    JavaScript
    async function retryFetch(fn, retries = 3, delay = 300) {
      try {
        return await fn();
      } catch (err) {
        if (retries === 0) throw err;
        console.log(`Retrying... ${retries} attempts left`);
        await new Promise(r => setTimeout(r, delay));
        return retryFetch(fn, retries - 1, delay * 2);
      }
    }
    
    // Usage with exponential backoff
    retryFetch(() => 
      fetch("https://jsonplaceholder.typicode.com/posts/1")
        .then(r => r.json())
    )
      .then(data => console.log("Success:", data.title))
      .cat
    ...

    Creating an API Client with Token Support

    Most real APIs require authentication (JWT, OAuth, API keys). Here's an authentication-aware wrapper that automatically injects the stored token into every request.

    Token Support

    API client with authentication

    Try it Yourself »
    JavaScript
    async function apiClient(path, options = {}) {
      const token = localStorage.getItem("token");
    
      const response = await fetch(path, {
        headers: {
          "Content-Type": "application/json",
          Authorization: token ? `Bearer ${token}` : "",
          ...options.headers
        },
        ...options
      });
    
      if (!response.ok) {
        throw new Error(`API Error: ${response.status}`);
      }
    
      return response.json();
    }
    
    // Simulate storing a token
    localStorage.setItem("token", "my-secret-jwt-token");
    
    // Now al
    ...

    Caching Responses for Faster Performance

    Caching results locally can reduce API load and make your UI feel instant. This memory cache prevents repeated network calls for unchanged data.

    Response Caching

    Implement in-memory response caching

    Try it Yourself »
    JavaScript
    const memoryCache = new Map();
    
    async function cachedRequest(key, fetcher) {
      if (memoryCache.has(key)) {
        console.log("Cache hit for:", key);
        return memoryCache.get(key);
      }
    
      console.log("Cache miss, fetching:", key);
      const data = await fetcher();
      memoryCache.set(key, data);
      return data;
    }
    
    // First call - fetches from network
    cachedRequest("users", () =>
      fetch("https://jsonplaceholder.typicode.com/users").then(r => r.json())
    ).then(users => console.log("First call:", users.l
    ...

    Data Normalization & Transformers

    Sometimes an API returns data in a structure that's not ideal for your UI. Instead of refactoring your components every time, you can normalize data inside the API client. This pattern is used by Redux Toolkit, Prisma, and GraphQL clients.

    Data Normalization

    Transform API responses for your UI

    Try it Yourself »
    JavaScript
    function normalizeUser(raw) {
      return {
        id: raw.id,
        name: raw.name,
        email: raw.email,
        isActive: raw.status === "active" || true,
        company: raw.company?.name || "Unknown"
      };
    }
    
    async function getUser(id) {
      const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
      const data = await response.json();
      return normalizeUser(data);
    }
    
    // Get normalized user data
    getUser(1).then(user => {
      console.log("Normalized user:", user);
      console.log("Company:
    ...

    Batching Multiple Requests in Parallel

    Often you need to load several resources at once—user data, analytics, settings, notifications. Using Promise.all() instead of sequential awaits dramatically reduces waiting time and is essential for dashboards and homepages.

    Parallel Requests

    Batch multiple API calls with Promise.all

    Try it Yourself »
    JavaScript
    // Sequential (slow) - each waits for the previous
    async function loadSequential() {
      console.time("Sequential");
      const users = await fetch("https://jsonplaceholder.typicode.com/users").then(r => r.json());
      const posts = await fetch("https://jsonplaceholder.typicode.com/posts").then(r => r.json());
      const comments = await fetch("https://jsonplaceholder.typicode.com/comments").then(r => r.json());
      console.timeEnd("Sequential");
      return { users, posts, comments };
    }
    
    // Parallel (fast) - 
    ...

    Implementing a Global API Error Handler

    Instead of manually catching errors everywhere, create a single centralized handler that maps error types to user-friendly messages. This keeps your UI clean and consistent.

    Global Error Handler

    Centralized API error handling

    Try it Yourself »
    JavaScript
    function handleApiError(error) {
      if (error.name === "AbortError") {
        return "Request timed out. Please try again.";
      }
      if (error.message.includes("401")) {
        return "You are not authorized. Please log in.";
      }
      if (error.message.includes("404")) {
        return "Resource not found.";
      }
      if (error.message.includes("500")) {
        return "Server error. Please try again later.";
      }
      return "An unexpected error occurred.";
    }
    
    // Test with different scenarios
    async function testErrorHandl
    ...

    Creating Reusable API Classes

    For even more structure, advanced developers wrap everything inside classes. This pattern makes every API call clean and semantic, following the same approach used in large-scale frontends.

    Reusable API Classes

    Object-oriented API client design

    Try it Yourself »
    JavaScript
    class Api {
      constructor(baseUrl) {
        this.baseUrl = baseUrl;
      }
    
      async get(path) {
        const response = await fetch(`${this.baseUrl}${path}`);
        if (!response.ok) throw new Error(`GET failed: ${response.status}`);
        return response.json();
      }
    
      async post(path, body) {
        const response = await fetch(`${this.baseUrl}${path}`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body)
        });
        if (!response.ok) throw new Error(
    ...

    Handling Paginated APIs

    Real APIs often return paginated data. Here's a reusable paginator that fetches multiple pages and combines the results—perfect for tables, infinite scroll, and analytic dashboards.

    Pagination Handling

    Fetch and combine paginated API data

    Try it Yourself »
    JavaScript
    async function fetchPage(url, page = 1, limit = 10) {
      const response = await fetch(`${url}?_page=${page}&_limit=${limit}`);
      return response.json();
    }
    
    async function fetchAllPages(url, maxPages = 3) {
      const results = [];
    
      for (let i = 1; i <= maxPages; i++) {
        console.log(`Fetching page ${i}...`);
        const page = await fetchPage(url, i, 5);
        if (page.length === 0) break;
        results.push(...page);
      }
    
      return results;
    }
    
    // Fetch first 3 pages of posts (5 per page)
    fetchAllPages
    ...

    Creating an API Cache With Expiry (TTL)

    To prevent stale data, add expiry times (Time-To-Live) to your cache entries. This ensures data stays fresh while still reducing unnecessary network requests.

    Cache with Expiry

    Implement TTL-based response caching

    Try it Yourself »
    JavaScript
    const timedCache = new Map();
    
    function cacheSet(key, value, ttl = 60000) {
      timedCache.set(key, { value, expiry: Date.now() + ttl });
      console.log(`Cached "${key}" for ${ttl/1000}s`);
    }
    
    function cacheGet(key) {
      const entry = timedCache.get(key);
      if (!entry) {
        console.log(`Cache miss: "${key}"`);
        return null;
      }
      if (Date.now() > entry.expiry) {
        timedCache.delete(key);
        console.log(`Cache expired: "${key}"`);
        return null;
      }
      console.log(`Cache hit: "${key}"`);
      re
    ...

    Smart Request Deduplication

    Imagine multiple components requesting the same data at the same time. Instead of sending the same HTTP request repeatedly, deduplication ensures only one network call happens. This prevents duplicate traffic and speeds load times dramatically.

    Request Deduplication

    Prevent duplicate concurrent requests

    Try it Yourself »
    JavaScript
    const pending = new Map();
    
    async function dedupedFetch(url) {
      if (pending.has(url)) {
        console.log("Reusing pending request for:", url);
        return pending.get(url);
      }
    
      console.log("Starting new request for:", url);
      const promise = fetch(url)
        .then(r => r.json())
        .finally(() => pending.delete(url));
      
      pending.set(url, promise);
      return promise;
    }
    
    // Simulate multiple components requesting the same data
    const url = "https://jsonplaceholder.typicode.com/users";
    
    // All thr
    ...

    Building a Universal API Wrapper

    Here's a complete, production-grade API wrapper that includes retries, timeouts, aborting, caching, JSON parsing, error mapping, and data transformation. This is the same architecture used by professional engineering teams.

    Universal API Wrapper

    Production-grade API client with all features

    Try it Yourself »
    JavaScript
    async function universalAPI(url, {
      method = "GET",
      body = null,
      headers = {},
      timeout = 8000,
      retries = 3,
      cache = false,
      transform = (x) => x
    } = {}) {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), timeout);
    
      // Check cache first
      if (cache && sessionStorage.getItem(url)) {
        console.log("Returning cached data");
        clearTimeout(timer);
        return JSON.parse(sessionStorage.getItem(url));
      }
    
      for (let attempt = 0; attempt 
    ...

    Security Considerations

    ⚠️ Security Warning: Browser storage is not secure for sensitive data.

    • Never store passwords — hash on the server, not the client
    • Sanitize user input — prevent XSS and injection attacks
    • Validate server responses — don't trust external APIs blindly
    • Limit localStorage data — store tokens only, no sensitive user data
    • Use HTTPS everywhere — modern browsers block insecure API calls

    What You Learned

    Core Patterns

    • âś” Building API layers
    • âś” Creating fetch wrappers
    • âś” Using async/await effectively
    • âś” Handling errors correctly
    • âś” Adding timeouts
    • âś” Implementing retries

    Advanced Techniques

    • âś” Injecting auth tokens
    • âś” Caching with expiration
    • âś” Request deduplication
    • âś” Parallel requests
    • âś” Pagination handling
    • âś” Universal API wrappers

    Practice Challenges

    1. Build a Complete API Client

    Create an API client that combines timeout, retry, caching, and token injection into a single reusable module.

    2. Implement Optimistic UI Updates

    Build a "like" button that updates instantly, then syncs with the server in the background with rollback on failure.

    3. Create a Request Queue

    Build a system that ensures only one API request runs at a time to prevent rate limiting and server overload.

    4. Design a Stale-While-Revalidate Cache

    Serve cached data instantly while fetching fresh data in the background, then update the UI when new data arrives.

    🎉 Lesson Complete!

    You now know how to build enterprise-grade API clients with retries, caching, and token injection. Next up: Error Handling Patterns.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy Policy • Terms of Service