Practice API Calls
Six drills: a basic fetch, async/await error handling, a rendered JSON list, a POST request, a React component with loading and error states, and a debounced search that cancels stale requests. Work each one in a free Scrimba scrim before checking the solution. This is for anyone who can render static markup but hasn't wired it to live data.
Before you start
You should know functions, arrays, and basic React (props, useState). Keep the Learn JavaScript course (opens in a new tab) open for the vanilla-JS drills, or Learn React (opens in a new tab) for the React ones (see Learn JavaScript and Learn React for what each covers). Both are free, no card needed, and the one-time 20% banner can be dismissed. A scrim also works as a live scratchpad: pause, edit the code, and run it.
Drills
Drill 1: Basic fetch with response.ok
Fetch a single post and log it, but only after checking the response succeeded.
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => {
// check response.ok here
})
.then((data) => console.log(data));
What you should see: the post object logged, or a thrown error on failure.
Solution
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => {
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json();
})
.then((data) => console.log(data))
.catch((error) => console.error(error));
fetch only rejects on a network failure, never a 404 or 500, so response.ok is what catches a bad status.
Drill 2: async/await with try/catch
Rewrite a fetch call as an async function using await and try/catch.
async function getUser(id) {
// fetch users/{id}, return JSON or null on failure
}
What you should see: getUser(1) logs a user object; getUser(9999) logs null instead of throwing.
Solution
async function getUser(id) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!response.ok) {
throw new Error(`User ${id} not found`);
}
return await response.json();
} catch (error) {
console.error(error.message);
return null;
}
}
await pauses execution until the promise settles. Common bug: forgetting response.json() also needs an await.
Drill 3: Render a list from JSON in vanilla JS
Fetch a list of to-dos and render their titles as <li> elements inside an existing <ul>.
<ul id="todo-list"></ul>
<script>
async function loadTodos() {
// fetch https://jsonplaceholder.typicode.com/todos?_limit=5
// append each todo's title as an <li> in #todo-list
}
loadTodos();
</script>
What you should see: five list items appear on the page, each showing a todo title.
Solution
<script>
async function loadTodos() {
const list = document.getElementById("todo-list");
const response = await fetch("https://jsonplaceholder.typicode.com/todos?_limit=5");
const todos = await response.json();
todos.forEach((todo) => {
const li = document.createElement("li");
li.textContent = todo.title;
list.appendChild(li);
});
}
loadTodos();
</script>
createElement and textContent keep API strings out of innerHTML, avoiding an injection bug. Loop only after .json() resolves: the response is still a stream before that.
Drill 4: POST with headers and a JSON body
Send a new post with a JSON body and the correct Content-Type header.
async function createPost(title, body) {
// POST to https://jsonplaceholder.typicode.com/posts
// send { title, body } as JSON
// return the created post
}
What you should see: the API echoes back the object plus a generated id (this endpoint fakes the save).
Solution
async function createPost(title, body) {
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, body }),
});
if (!response.ok) {
throw new Error(`Create failed: ${response.status}`);
}
return response.json();
}
A request body must be a string, so run it through JSON.stringify first. Don't skip Content-Type: backends use it to decide how to parse the body.
Drill 5: Fetch in React with loading, error, and empty states
Build a component that shows loading, error, empty, or the list, depending on fetch state.
function UserList() {
// state: users, loading, error
// fetch https://jsonplaceholder.typicode.com/users on mount
return <div>{/* render loading, error, empty, or the list */}</div>;
}
What you should see: "Loading..." briefly, then a list of ten user names.
Solution
import { useState, useEffect } from "react";
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadUsers() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) throw new Error("Failed to load users");
setUsers(await response.json());
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
loadUsers();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
if (users.length === 0) return <p>No users found.</p>;
return (
<ul>
{users.map((user) => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
finally resets loading on both success and failure. Check error before the empty check so a failed fetch never shows as "no results."
Drill 6: Debounced search that cancels in-flight requests
Wire a search input to the API, but debounce the calls and abort any request still pending when a new one starts.
function Search() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
// debounce the query, then fetch
// https://dummyjson.com/products/search?q={query}
// abort the previous request if the query changes first
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>{results.map((r) => <li key={r.id}>{r.title}</li>)}</ul>
</div>
);
}
What you should see: results update about 300ms after you stop typing, with no flicker from stale responses.
Solution
import { useState, useEffect } from "react";
function Search() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) return setResults([]);
const controller = new AbortController();
const timeoutId = setTimeout(async () => {
try {
const url = `https://dummyjson.com/products/search?q=${query}`;
const response = await fetch(url, { signal: controller.signal });
const data = await response.json();
setResults(data.products);
} catch (error) {
if (error.name !== "AbortError") console.error(error);
}
}, 300);
return () => {
clearTimeout(timeoutId);
controller.abort();
};
}, [query]);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>{results.map((r) => <li key={r.id}>{r.title}</li>)}</ul>
</div>
);
}
Cleanup runs before each new keystroke's effect, clearing the timeout and aborting the previous fetch. Skip the AbortError check and every keystroke logs a harmless but scary error.
Common mistakes
- Treating a 404/500 as success. Check
response.okfirst. - Forgetting
awaiton.json(). It's a promise too. - Sending a raw object as a POST body. Use
JSON.stringifyand setContent-Type. - Leaving
loadingstuck ontrueafter an error. Reset it infinally. - Racing fetches in a search box. Debounce and
AbortControllercancel stale requests.
Where to practice next on Scrimba
| Course | What it drills | Access | Length |
|---|---|---|---|
| Learn JavaScript | The vanilla-JS ground under Drills 1 to 4: functions, arrays, rendering with createElement, localStorage in the Chrome extension module. fetch itself is not taught here. All 190-odd scrims free; Pro gates three Solo Projects | Free | 9.4 hrs |
| Advanced JavaScript | Module 4, Asynchronous JavaScript & APIs (84 min, 24 scrims): a dog-image fetch with .then() chains and again with async/await, .catch() and .finally(), a try/catch challenge, Promise.all. Five sample scrims free, the rest Pro | Pro | 9.8 hrs |
| Learn React | Section 4, Side Effects (108 min, 22 scrims): a meme generator fetching templates from an API, useEffect with and without a dependencies array, and cleanup | Free | 15.1 hrs |
| Build a React Project: Movie Search App | An async fetch to TMDB inside try/catch on form submit, useState for the query and the results, and a MovieCard props challenge. No debouncing. The first four scrims are free; the fetch scrim onward is Pro | Pro | 57 min |
| Learn Node.js | The other side of Drill 4: a REST API with path parameters and query strings, then a server that parses a POST body chunk by chunk. All 49 scrims free | Free | 3.5 hrs |
API integration sits in the Frontend Developer Path: the skill that turns static components into apps talking to a real backend.
Related practice guides
- All practice drills
- Practice React Hooks, the
useEffect/useStatepatterns these drills use - React Practice Projects, full API-powered apps
No, JSONPlaceholder and DummyJSON are free public APIs with no signup.
fetch only rejects on a network failure. A 404 or 500 still resolves, so response.ok is what catches a bad status code.
Either works, but async/await reads more clearly for dependent requests.
An ignored request still completes and can overwrite a newer result; AbortController stops it.
Learn JavaScript. It covers fetch and promises before Learn React layers useEffect on top.
Practice fetch and async/await inside real mini-apps
Start with the free JavaScript and React courses; add Pro for the movie search project and beyond.