Scenario

Pick a public API with no authentication required (a countries API, a jokes API, a books API — the exact choice doesn’t matter), or build a small local JSON file and fetch it as a stand-in. Build a page that loads a list from it, lets the user search or filter the results client-side, and clearly communicates what’s happening at every stage of the request — not just the happy path.

Requirements

Core

  • On load, the page fetches data and shows a visible loading state until it resolves.
  • A successful response with results renders them as a list (cards, table rows — your choice).
  • A successful response with zero results shows a distinct, honest empty state — not a blank screen and not the loading state left on forever.
  • A failed request (network error, non-OK status) shows a clear error message, not a silent failure or an uncaught rejection in the console.
  • A search or filter input narrows the already-fetched results client-side, without re-fetching on every keystroke.
  • Rendering never trusts API data blindly: a missing field doesn’t crash the page (defensive rendering — check before you assume a property exists).

Stretch

  • Debounce the search input so filtering doesn’t run on every single keystroke.
  • Add a second, independent fetch (a related endpoint, or a details view fetched on click) and handle its loading/error states separately from the first.
  • Use Promise.all to fetch two endpoints concurrently and combine their results.

Reflect

  • Where in your code is “we have zero results” distinguished from “we’re still loading” from “the request failed”? Are those three states represented explicitly, or inferred from other variables in a way that could go wrong?
  • What would break if the API’s response shape changed slightly (a renamed field, a nested object instead of a flat one)? How much of your rendering code would need to change?

Suggested milestones

  1. Fetch and console.log the raw response — confirm the request and data shape before rendering anything.
  2. Render the list for the successful case only.
  3. Add the loading state, shown before the fetch resolves.
  4. Add the empty and error states.
  5. Add client-side search/filtering over the already-fetched data.

Acceptance checks

  • Throttling the network to “slow 3G” in DevTools shows the loading state for a visible period before results appear.
  • A search term matching nothing shows the empty state, not a blank area.
  • Turning off network access (or pointing the fetch at a bad URL) shows the error state, not a console-only failure.
  • Typing in the search box narrows the visible list without triggering a new network request each time (check the Network tab).