π 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.
(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 |
|---|---|---|
GET | Read / retrieve data | Get a list of users |
POST | Create new data | Create a new user |
PUT / PATCH | Update existing data | Update a user's email |
DELETE | Remove data | Delete 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
(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 JSON | API data (most common) |
response.text() | Promise β string | Plain text, HTML |
response.blob() | Promise β Blob | Images, files |
response.arrayBuffer() | Promise β ArrayBuffer | Binary 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
});
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 error | No internet, DNS failure, CORS block | Promise rejects (catch runs) |
| HTTP 4xx | Bad request, not found, unauthorized | Promise resolves (ok = false) |
| HTTP 5xx | Server error, timeout | Promise resolves (ok = false) |
| JSON parse error | Response 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.
β³ 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(notinnerHTML) 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 checkresponse.okasync/awaitmakes Promise-based code read like synchronous code- Use
try/catchwith 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
URLSearchParamsto 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
- MDN β Fetch API
- MDN β Promise
- MDN β Asynchronous JavaScript
- MDN β URLSearchParams
- javascript.info β Fetch
- javascript.info β Async/Await
- JSONPlaceholder β Free REST API for Testing
π 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.