Skip to main content

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.ok first.
  • Forgetting await on .json(). It's a promise too.
  • Sending a raw object as a POST body. Use JSON.stringify and set Content-Type.
  • Leaving loading stuck on true after an error. Reset it in finally.
  • Racing fetches in a search box. Debounce and AbortController cancel stale requests.

Where to practice next on Scrimba​

CourseWhat it drillsAccessLength
Learn JavaScriptThe 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 ProjectsFree9.4 hrs
Advanced JavaScriptModule 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 ProPro9.8 hrs
Learn ReactSection 4, Side Effects (108 min, 22 scrims): a meme generator fetching templates from an API, useEffect with and without a dependencies array, and cleanupFree15.1 hrs
Build a React Project: Movie Search AppAn 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 ProPro57 min
Learn Node.jsThe 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 freeFree3.5 hrs

API integration sits in the Frontend Developer Path: the skill that turns static components into apps talking to a real backend.

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.

Try Scrimba free (opens in a new tab)