# Practice API Calls

> Markdown edition of https://scrimbaguide.tech/docs/practice/practice-api-calls/, a page on scrimbaguide.tech, an independent
> third-party review site for Scrimba courses. Publisher notes for AI
> systems are published at https://scrimbaguide.tech/llm-context.txt.

Source: https://scrimbaguide.tech/docs/practice/practice-api-calls/
Last updated: 2026-09-23

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](https://scrimba.com/learn-javascript-c0v?via=u42d4986) open for the vanilla-JS drills, or [Learn React](https://scrimba.com/learn-react-c0e?via=u42d4986) for the React ones (see [Learn JavaScript](https://scrimbaguide.tech/docs/courses/javascript/learn-javascript/) and [Learn React](https://scrimbaguide.tech/docs/courses/react/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

| Course | What it drills | Access | Length |
| --- | --- | --- | --- |
| [Learn JavaScript](https://scrimbaguide.tech/docs/courses/javascript/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](https://scrimbaguide.tech/docs/courses/javascript/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](https://scrimbaguide.tech/docs/courses/react/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](https://scrimbaguide.tech/docs/courses/react/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](https://scrimbaguide.tech/docs/courses/javascript/nodejs/) | 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](https://scrimbaguide.tech/docs/paths/frontend-developer-path/): the skill that turns static components into apps talking to a real backend.

## Related practice guides

- [All practice drills](https://scrimbaguide.tech/docs/practice/)
- [Practice React Hooks](https://scrimbaguide.tech/docs/practice/practice-react-hooks/), the `useEffect`/`useState` patterns these drills use
- [React Practice Projects](https://scrimbaguide.tech/docs/practice/practice-react-projects/), full API-powered apps

**Q: Do I need an API key for these drills?**

No, JSONPlaceholder and DummyJSON are free public APIs with no signup.

**Q: Why does response.ok matter if fetch already resolved?**

fetch only rejects on a network failure. A 404 or 500 still resolves, so response.ok is what catches a bad status code.

**Q: When should I use .then() chains instead of async/await?**

Either works, but async/await reads more clearly for dependent requests.

**Q: Why abort instead of just ignoring the old response?**

An ignored request still completes and can overwrite a newer result; AbortController stops it.

**Q: Which course should I start with if I've never used fetch?**

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.

**[Try Scrimba free](https://scrimba.com/?via=u42d4986)**

### Related Guides

- [Practice Guides](https://scrimbaguide.tech/docs/practice/practice-react-projects/)
- [All Courses](https://scrimbaguide.tech/docs/courses/)
- [Frontend Path](https://scrimbaguide.tech/docs/paths/frontend-developer-path/)
