Skip to main content

🌐 Lesson 20: Working with APIs & Fetch

So far, everything your apps know comes from code you wrote or data the user entered. APIs change that β€” they let your pages talk to servers, pull in live data, and connect to the wider internet. In this lesson you'll learn Promises, async/await, and the Fetch API.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what an API is and how REST APIs work
  • Understand Promises and their three states (pending, fulfilled, rejected)
  • Use fetch() to make HTTP requests and process JSON responses
  • Write cleaner asynchronous code with async/await
  • Handle errors gracefully with try/catch and user-friendly messages
  • Display fetched data in the DOM with loading and error states
  • Work with query parameters, headers, and POST requests

Estimated Time: 60 minutes

Project: Build a GitHub user search app

πŸ“‘ In This Lesson

What Is an API?

An API (Application Programming Interface) is a set of rules that lets one piece of software talk to another. When we say "API" in front-end development, we usually mean a web API β€” a server that sends and receives data over HTTP, typically in JSON format.

How It Works

Your JavaScript sends an HTTP request to a URL (called an endpoint). The server processes it and sends back an HTTP response containing the data you asked for.

graph LR A["Your JavaScript
(Client)"] -->|"HTTP Request
GET /api/users"| B["Server
(API)"] B -->|"HTTP Response
JSON data"| A style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style B fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b

REST APIs & HTTP Methods

Most web APIs follow the REST pattern, which maps standard HTTP methods to data operations.

HTTP Method Purpose Example
GETRead / retrieve dataGet a list of users
POSTCreate new dataCreate a new user
PUT / PATCHUpdate existing dataUpdate a user's email
DELETERemove dataDelete a user

JSON β€” The Language of APIs

APIs almost always send data as JSON (JavaScript Object Notation). It looks like JavaScript objects and arrays β€” because that's exactly what it's based on.


// JSON from an API response
{
    "id": 1,
    "username": "ray",
    "email": "ray@example.com",
    "projects": ["course-builder", "pomodoro-app"],
    "active": true
}
                

πŸ’‘ Free APIs to Practice With

You don't need to build your own server to start learning. Here are some free, public APIs:

  • JSONPlaceholder β€” Fake REST API for testing (users, posts, comments)
  • GitHub API β€” Public user and repo data (no auth needed for basic queries)
  • PokΓ©API β€” PokΓ©mon data
  • Dog CEO β€” Random dog images

Promises β€” Handling Async Operations

Network requests take time β€” you can't just pause JavaScript while waiting for a server to respond (that would freeze the entire page). Instead, JavaScript uses Promises to handle operations that complete in the future.

What Is a Promise?

A Promise is an object that represents a value you don't have yet but expect to get later. It's like a receipt from a restaurant β€” "your order is being prepared, we'll call your number when it's ready."


// A Promise has three states:
// 1. Pending   β€” still working, no result yet
// 2. Fulfilled β€” completed successfully, has a value
// 3. Rejected  β€” something went wrong, has an error
                
graph LR A["Promise Created
(Pending)"] --> B{"Async operation
completes"} B -->|Success| C["Fulfilled
(.then runs)"] B -->|Failure| D["Rejected
(.catch runs)"] style A fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style B fill:#f3e8ff,stroke:#a855f7,stroke-width:2px,color:#1e293b style C fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style D fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#1e293b

Using .then() and .catch()


// fetch() returns a Promise
const promise = fetch("https://jsonplaceholder.typicode.com/users/1");

// .then() runs when the Promise is fulfilled
promise
    .then(response => {
        console.log("Got a response!", response);
        return response.json();  // This also returns a Promise!
    })
    .then(data => {
        console.log("User data:", data);
        console.log("Name:", data.name);
    })
    .catch(error => {
        // .catch() runs if anything in the chain fails
        console.error("Something went wrong:", error);
    })
    .finally(() => {
        // .finally() runs no matter what β€” success or failure
        console.log("Request complete");
    });
                

Promise Chaining

Each .then() returns a new Promise, so you can chain them. The return value of one .then() becomes the input of the next.


fetch("https://jsonplaceholder.typicode.com/users/1")
    .then(response => response.json())          // Parse JSON
    .then(user => {
        console.log(user.name);                 // Use the data
        return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`);
    })
    .then(response => response.json())          // Parse second response
    .then(posts => {
        console.log(`${posts.length} posts`);   // Use second data
    })
    .catch(error => {
        console.error("Error:", error);         // Catches ANY error in the chain
    });
                

⚠️ Common Mistake: Forgetting to Return

Inside a .then(), you must return a value (or Promise) for the next .then() in the chain to receive it. Forgetting return means the next .then() gets undefined.


// ❌ Missing return β€” next .then() gets undefined
.then(response => {
    response.json();  // No return!
})
.then(data => {
    console.log(data);  // undefined!
});

// βœ… Return the value
.then(response => {
    return response.json();
})
// Or use arrow function shorthand (implicit return)
.then(response => response.json())
                    

The Fetch API

fetch() is the modern, built-in way to make HTTP requests in JavaScript. It's available in all modern browsers β€” no libraries needed.

Basic GET Request


// Fetch returns a Promise that resolves to a Response object
fetch("https://jsonplaceholder.typicode.com/users")
    .then(response => response.json())
    .then(users => {
        console.log(users);  // Array of user objects
    });
                

The Response Object

fetch() resolves to a Response object β€” not the data itself. You need to call a method to extract the body.


fetch("https://jsonplaceholder.typicode.com/users/1")
    .then(response => {
        console.log(response.status);     // 200
        console.log(response.ok);         // true (status 200–299)
        console.log(response.statusText); // "OK"
        console.log(response.headers);    // Headers object
        console.log(response.url);        // The final URL

        return response.json();  // Parse the body as JSON
    })
    .then(data => {
        console.log(data);  // Now you have the actual data
    });
                

Response Body Methods

Method Returns Use For
response.json()Promise β†’ parsed JSONAPI data (most common)
response.text()Promise β†’ stringPlain text, HTML
response.blob()Promise β†’ BlobImages, files
response.arrayBuffer()Promise β†’ ArrayBufferBinary data

⚠️ fetch() Doesn't Throw on HTTP Errors

This surprises everyone. fetch() only rejects the Promise for network failures (no internet, DNS error, server unreachable). A 404 Not Found or 500 Server Error is still a successful HTTP response β€” fetch() resolves normally. You have to check response.ok yourself.


fetch("https://jsonplaceholder.typicode.com/users/9999")
    .then(response => {
        // This runs even for 404!
        if (!response.ok) {
            throw new Error(`HTTP error: ${response.status}`);
        }
        return response.json();
    })
    .then(data => console.log(data))
    .catch(error => console.error(error));
                    

Query Parameters


// Build URLs with query parameters
const baseUrl = "https://jsonplaceholder.typicode.com/posts";

// Option 1: String concatenation (simple but fragile)
fetch(`${baseUrl}?userId=1&_limit=5`);

// Option 2: URLSearchParams (safer, handles encoding)
const params = new URLSearchParams({
    userId: 1,
    _limit: 5,
    q: "hello world"  // Automatically encoded as "hello+world"
});
fetch(`${baseUrl}?${params}`);
                

POST Requests β€” Sending Data


fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify({
        title: "My New Post",
        body: "This is the content of my post.",
        userId: 1
    })
})
    .then(response => response.json())
    .then(data => {
        console.log("Created:", data);  // Server returns the new resource
    });
                
graph TD A["fetch(url, options)"] --> B["Returns Promise"] B --> C["Resolves to
Response object"] C --> D{"response.ok?"} D -->|Yes| E["response.json()"] D -->|No| F["throw Error"] E --> G["Returns Promise"] G --> H["Resolves to
parsed data"] F --> I[".catch() handles it"] style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style B fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style C fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style D fill:#f3e8ff,stroke:#a855f7,stroke-width:2px,color:#1e293b style E fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style F fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#1e293b style G fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style H fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style I fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#1e293b

async/await β€” Cleaner Async Code

async/await is syntactic sugar on top of Promises. It lets you write asynchronous code that reads like synchronous code β€” no chains of .then() callbacks.

The Basics


// 1. Mark the function as "async"
async function getUser(id) {
    // 2. "await" pauses until the Promise resolves
    const response = await fetch(
        `https://jsonplaceholder.typicode.com/users/${id}`
    );

    // 3. Check for errors (fetch doesn't throw on HTTP errors)
    if (!response.ok) {
        throw new Error(`User not found: ${response.status}`);
    }

    // 4. Await the JSON parsing too
    const user = await response.json();

    return user;  // Async functions always return a Promise
}

// Calling an async function
getUser(1).then(user => console.log(user.name));
                

Comparing .then() vs. async/await


// With .then() chains
function getUserPostsThen(userId) {
    return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
        .then(response => response.json())
        .then(user => {
            console.log(`Posts by ${user.name}:`);
            return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`);
        })
        .then(response => response.json())
        .then(posts => {
            posts.forEach(post => console.log(`- ${post.title}`));
            return posts;
        });
}

// With async/await β€” same logic, much easier to read
async function getUserPostsAsync(userId) {
    const userResponse = await fetch(
        `https://jsonplaceholder.typicode.com/users/${userId}`
    );
    const user = await userResponse.json();
    console.log(`Posts by ${user.name}:`);

    const postsResponse = await fetch(
        `https://jsonplaceholder.typicode.com/posts?userId=${userId}`
    );
    const posts = await postsResponse.json();
    posts.forEach(post => console.log(`- ${post.title}`));

    return posts;
}
                

Parallel Requests with Promise.all

When requests don't depend on each other, run them in parallel instead of one after another. Promise.all waits for all Promises to resolve.


async function getDashboardData() {
    // ❌ Sequential β€” slow (waits for each to finish)
    const users = await fetch("/api/users").then(r => r.json());
    const posts = await fetch("/api/posts").then(r => r.json());
    const comments = await fetch("/api/comments").then(r => r.json());
    // Total time: user + posts + comments

    // βœ… Parallel β€” fast (all three run at the same time)
    const [usersData, postsData, commentsData] = await Promise.all([
        fetch("/api/users").then(r => r.json()),
        fetch("/api/posts").then(r => r.json()),
        fetch("/api/comments").then(r => r.json())
    ]);
    // Total time: whichever is slowest
}
                

πŸ’‘ await Only Works Inside async Functions

You can only use await inside a function marked with async. If you try to use it at the top level of a regular script, you'll get a syntax error. However, modules (<script type="module">) support top-level await.


// ❌ Error in a regular script
const data = await fetch("/api/users");  // SyntaxError!

// βœ… Wrap in an async function
async function init() {
    const data = await fetch("/api/users");
}
init();

// βœ… Or use an async IIFE (Immediately Invoked Function Expression)
(async () => {
    const data = await fetch("/api/users");
})();
                    

Error Handling

Network requests fail β€” the user might be offline, the server could be down, or the API might return an error. Robust error handling isn't optional; it's what separates a good app from a broken one.

try/catch with async/await


async function fetchUser(id) {
    try {
        const response = await fetch(
            `https://jsonplaceholder.typicode.com/users/${id}`
        );

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

        const user = await response.json();
        return user;

    } catch (error) {
        // Handles BOTH network errors and our thrown errors
        console.error("Failed to fetch user:", error.message);
        return null;  // Return a fallback value
    }
}
                

Error Types You'll Encounter

Error Type Cause fetch() Behavior
Network errorNo internet, DNS failure, CORS blockPromise rejects (catch runs)
HTTP 4xxBad request, not found, unauthorizedPromise resolves (ok = false)
HTTP 5xxServer error, timeoutPromise resolves (ok = false)
JSON parse errorResponse isn't valid JSON.json() rejects

A Reusable Fetch Wrapper


async function apiFetch(url, options = {}) {
    try {
        const response = await fetch(url, options);

        if (!response.ok) {
            // Try to get error details from the response body
            const errorBody = await response.text();
            throw new Error(
                `HTTP ${response.status}: ${errorBody || response.statusText}`
            );
        }

        return await response.json();

    } catch (error) {
        // Re-throw with a cleaner message
        if (error.name === "TypeError") {
            throw new Error("Network error β€” check your connection");
        }
        throw error;
    }
}

// Usage β€” clean and consistent
try {
    const users = await apiFetch("https://jsonplaceholder.typicode.com/users");
    console.log(users);
} catch (error) {
    console.error(error.message);
}
                

πŸ’‘ What Is CORS?

CORS (Cross-Origin Resource Sharing) is a security feature that prevents your JavaScript from making requests to a different domain unless that domain explicitly allows it. If you see a CORS error in the console, it means the API server hasn't allowed requests from your origin. You can't fix CORS from the client β€” it's a server-side configuration. Public APIs like JSONPlaceholder and the GitHub API have CORS enabled for everyone.

Displaying Fetched Data

Fetching data is only half the job. You also need to show it to the user β€” with proper loading states, error messages, and dynamic DOM updates.

The Three States of a Data Request

Every fetch operation has three possible UI states. Good apps handle all three.

graph LR A["Loading
⏳ Spinner"] -->|Success| B["Data
πŸ“‹ Content"] A -->|Failure| C["Error
❌ Message"] C -->|Retry| A style A fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style B fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style C fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#1e293b

Complete Example: Fetching & Displaying Users


const container = document.querySelector("#user-list");

async function loadUsers() {
    // 1. Show loading state
    container.innerHTML = '<p class="loading">Loading users...</p>';

    try {
        // 2. Fetch data
        const response = await fetch(
            "https://jsonplaceholder.typicode.com/users"
        );
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const users = await response.json();

        // 3. Render data
        container.innerHTML = "";
        users.forEach(user => {
            const card = document.createElement("div");
            card.classList.add("user-card");
            card.innerHTML = `
                <h3>${escapeHTML(user.name)}</h3>
                <p>${escapeHTML(user.email)}</p>
                <p>${escapeHTML(user.company.name)}</p>
            `;
            container.append(card);
        });

    } catch (error) {
        // 4. Show error state
        container.innerHTML = `
            <div class="error-state">
                <p>Failed to load users: ${escapeHTML(error.message)}</p>
                <button id="retry-btn">Try Again</button>
            </div>
        `;
        document.querySelector("#retry-btn")
            .addEventListener("click", loadUsers);
    }
}

loadUsers();
                

Escaping HTML β€” Preventing XSS

When inserting API data into innerHTML, always escape it. If an API returns malicious HTML, it could execute as code on your page.


function escapeHTML(str) {
    const div = document.createElement("div");
    div.textContent = str;
    return div.innerHTML;
}

// Or use textContent instead of innerHTML (safer by default)
const el = document.createElement("p");
el.textContent = apiData.name;  // Can't inject HTML
                

Rendering Lists from API Data


async function renderPosts() {
    const posts = await apiFetch(
        "https://jsonplaceholder.typicode.com/posts?_limit=10"
    );

    const list = document.querySelector("#post-list");
    list.innerHTML = "";

    // Use DocumentFragment for performance
    const fragment = document.createDocumentFragment();

    posts.forEach(post => {
        const article = document.createElement("article");
        article.classList.add("post");

        const title = document.createElement("h3");
        title.textContent = post.title;

        const body = document.createElement("p");
        body.textContent = post.body;

        article.append(title, body);
        fragment.append(article);
    });

    list.append(fragment);
}
                

βœ… Best Practices for Displaying API Data

  • Always show loading, data, and error states β€” never leave the user guessing
  • Use textContent (not innerHTML) for user-generated or API data to prevent XSS
  • Provide a retry button on error so users can recover without refreshing
  • Use DocumentFragment when rendering many items from API data
  • Show empty states when the API returns an empty array β€” "No results found"
  • Consider debouncing if the fetch is triggered by user input (search-as-you-type)

Hands-on Exercise

πŸ‹οΈ Exercise: GitHub User Search

Objective: Build a search app that fetches GitHub user profiles and displays their avatar, name, bio, and repo count. Uses the GitHub API (no auth needed), debounced search input, and loading/error/empty states.


<!-- Save this as github-search.html and open in a browser -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>GitHub User Search</title>
    <style>
        * { box-sizing: border-box; margin: 0; }
        body { font-family: system-ui, sans-serif; background: #0d1117; color: #c9d1d9; padding: 2rem; }
        .app { max-width: 600px; margin: 0 auto; }
        h1 { color: #f0f6fc; margin-bottom: 1.5rem; }
        .search-bar {
            display: flex; gap: 0.5rem; margin-bottom: 2rem;
        }
        .search-bar input {
            flex: 1; padding: 0.7rem 1rem; border: 1px solid #30363d;
            border-radius: 6px; background: #161b22; color: #c9d1d9;
            font-size: 1rem;
        }
        .search-bar input:focus { outline: none; border-color: #58a6ff; }
        .search-bar button {
            padding: 0.7rem 1.2rem; background: #238636; color: white;
            border: none; border-radius: 6px; font-size: 1rem; cursor: pointer;
        }
        .search-bar button:hover { background: #2ea043; }

        .user-card {
            display: flex; gap: 1.5rem; padding: 1.5rem;
            background: #161b22; border: 1px solid #30363d;
            border-radius: 10px; align-items: center;
        }
        .user-card img {
            width: 100px; height: 100px; border-radius: 50%;
            border: 2px solid #30363d;
        }
        .user-info h2 { color: #f0f6fc; margin-bottom: 0.25rem; font-size: 1.3rem; }
        .user-info .username { color: #58a6ff; margin-bottom: 0.5rem; font-size: 0.95rem; }
        .user-info .bio { color: #8b949e; margin-bottom: 0.75rem; font-size: 0.9rem; }
        .user-stats { display: flex; gap: 1.5rem; font-size: 0.85rem; color: #8b949e; }
        .user-stats span strong { color: #c9d1d9; }

        .status-message {
            text-align: center; padding: 2rem; color: #8b949e;
        }
        .status-message.error { color: #f85149; }
        .loading-spinner {
            display: inline-block; width: 20px; height: 20px;
            border: 2px solid #30363d; border-top-color: #58a6ff;
            border-radius: 50%; animation: spin 0.8s linear infinite;
        }
        @keyframes spin { to { transform: rotate(360deg); } }
    </style>
</head>
<body>
    <div class="app">
        <h1>πŸ” GitHub User Search</h1>
        <div class="search-bar">
            <input type="text" id="search-input" placeholder="Enter a GitHub username...">
            <button id="search-btn">Search</button>
        </div>
        <div id="result">
            <p class="status-message">Search for a GitHub user to see their profile.</p>
        </div>
    </div>

    <script>
    // TODO: Complete each task

    // 1. Get DOM references:
    //    - #search-input, #search-btn, #result

    // 2. Write showLoading():
    //    - Set result innerHTML to a loading spinner + "Searching..."

    // 3. Write showError(message):
    //    - Set result innerHTML to the error message with "error" class

    // 4. Write showUser(user):
    //    - Build a .user-card with:
    //      - Avatar image (user.avatar_url)
    //      - Name (user.name or "No name")
    //      - Username with @ prefix (user.login)
    //      - Bio (user.bio or "No bio available")
    //      - Stats: repos (user.public_repos), followers, following
    //    - Use textContent or escaping for all data!

    // 5. Write async searchUser(username):
    //    - Show loading
    //    - fetch https://api.github.com/users/{username}
    //    - If response.ok: parse JSON, call showUser
    //    - If response.status === 404: showError "User not found"
    //    - If other error: showError with the status
    //    - Catch network errors: showError "Network error"

    // 6. Hook up events:
    //    - Search button click β†’ searchUser
    //    - Enter key in input β†’ searchUser
    //    - Bonus: debounce the input event for auto-search

    // 7. Bonus: Add a link to the user's GitHub profile
    //    (user.html_url) in the card
    </script>
</body>
</html>
                    
πŸ’‘ Hint

3: The GitHub API URL is https://api.github.com/users/USERNAME. It returns JSON with fields like login, name, avatar_url, bio, public_repos, followers, following, html_url.

4: For the avatar, create an <img> element and set its src to user.avatar_url. Use textContent for all text to prevent XSS.

5: Check response.status === 404 separately to show "User not found" instead of a generic error. Wrap the whole thing in try/catch for network errors.

6: For auto-search, use the debounce function from Lesson 18 with a 500ms delay on the input event.

βœ… Solution

// 1. DOM references
const searchInput = document.querySelector("#search-input");
const searchBtn = document.querySelector("#search-btn");
const result = document.querySelector("#result");

// 2. Loading state
function showLoading() {
    result.innerHTML = `
        <p class="status-message">
            <span class="loading-spinner"></span> Searching...
        </p>
    `;
}

// 3. Error state
function showError(message) {
    result.innerHTML = `
        <p class="status-message error">${escapeHTML(message)}</p>
    `;
}

// Helper: escape HTML
function escapeHTML(str) {
    const div = document.createElement("div");
    div.textContent = str;
    return div.innerHTML;
}

// 4. Display user
function showUser(user) {
    const card = document.createElement("div");
    card.classList.add("user-card");

    const img = document.createElement("img");
    img.src = user.avatar_url;
    img.alt = `${user.login}'s avatar`;

    const info = document.createElement("div");
    info.classList.add("user-info");

    const name = document.createElement("h2");
    name.textContent = user.name || "No name";

    const username = document.createElement("p");
    username.classList.add("username");
    const link = document.createElement("a");
    link.href = user.html_url;
    link.target = "_blank";
    link.rel = "noopener";
    link.textContent = `@${user.login}`;
    link.style.color = "#58a6ff";
    link.style.textDecoration = "none";
    username.append(link);

    const bio = document.createElement("p");
    bio.classList.add("bio");
    bio.textContent = user.bio || "No bio available";

    const stats = document.createElement("div");
    stats.classList.add("user-stats");
    stats.innerHTML = `
        <span><strong>${user.public_repos}</strong> repos</span>
        <span><strong>${user.followers}</strong> followers</span>
        <span><strong>${user.following}</strong> following</span>
    `;

    info.append(name, username, bio, stats);
    card.append(img, info);

    result.innerHTML = "";
    result.append(card);
}

// 5. Search function
async function searchUser(username) {
    const trimmed = username.trim();
    if (!trimmed) return;

    showLoading();

    try {
        const response = await fetch(
            `https://api.github.com/users/${encodeURIComponent(trimmed)}`
        );

        if (response.status === 404) {
            showError(`User "${trimmed}" not found`);
            return;
        }

        if (!response.ok) {
            showError(`Server error: ${response.status}`);
            return;
        }

        const user = await response.json();
        showUser(user);

    } catch (error) {
        showError("Network error β€” check your connection");
    }
}

// 6. Event listeners
searchBtn.addEventListener("click", () => {
    searchUser(searchInput.value);
});

searchInput.addEventListener("keydown", (event) => {
    if (event.key === "Enter") {
        searchUser(searchInput.value);
    }
});

// Bonus: debounced auto-search
function debounce(fn, delay) {
    let timerId;
    return (...args) => {
        clearTimeout(timerId);
        timerId = setTimeout(() => fn(...args), delay);
    };
}

const autoSearch = debounce((value) => {
    if (value.trim().length >= 2) {
        searchUser(value);
    }
}, 500);

searchInput.addEventListener("input", (event) => {
    autoSearch(event.target.value);
});
                        

🎯 Quick Quiz

Question 1: What does fetch() return?

Question 2: What happens when fetch() gets a 404 response?

Question 3: What does the async keyword do to a function?

Question 4: Why use Promise.all() instead of multiple sequential await calls?

Question 5: What are the three UI states every fetch operation should handle?

Summary

πŸŽ‰ Key Takeaways

  • An API is a server that sends/receives data over HTTP, usually in JSON format
  • Promises represent values that arrive in the future β€” they can be pending, fulfilled, or rejected
  • fetch(url) returns a Promise that resolves to a Response object
  • Call response.json() to parse the body (this also returns a Promise)
  • fetch() does not reject on HTTP errors β€” always check response.ok
  • async/await makes Promise-based code read like synchronous code
  • Use try/catch with async/await for clean error handling
  • Use Promise.all() for parallel requests that don't depend on each other
  • Always handle three UI states: loading, data, and error
  • Use textContent (not innerHTML) when displaying API data to prevent XSS
  • Use URLSearchParams to build query strings safely

πŸ† Module 5 Complete!

You've finished the entire interactive features module! You now know how to:

  • Lesson 17: Capture form input and validate it with real-time feedback
  • Lesson 18: Schedule code with timers and create smooth animations
  • Lesson 19: Persist data in the browser with Local Storage
  • Lesson 20: Fetch live data from APIs and display it dynamically

These are the skills that turn a static web page into a real web application. In the final module, you'll combine everything into a complete project.

πŸ“š Additional Resources

πŸš€ What's Next?

It's time to put everything together. In the next lesson, you'll build a complete interactive project from scratch β€” combining DOM manipulation, events, forms, timers, Local Storage, and API calls into one polished application.