Skip to main content
Back to module
JavaScript Essentials · Asynchronous JavaScript

Fetching data from an API

Reading38mLesson 2 of 3 in this module

Request JSON with fetch, check the response, and render the results to the page safely.

Why this matters

Most web pages do not contain their data; they ask for it. A weather widget asks a weather service, a shop asks its product API, this site asks its own backend for your courses and progress. In the browser, the standard way to ask is fetch. Combined with async/await and the DOM skills from module 2, it lets you build pages that show live data.

What an API request looks like

An API (application programming interface) is a set of URLs that return data instead of web pages. You send an HTTP request to a URL and get back a response with:

  • a status code: 200 OK, 404 not found, 500 server error, and so on;
  • headers, such as Content-Type: application/json;
  • a body, usually JSON text.

fetch in three steps

async function loadUsers() {
  const res = await fetch("https://api.example.com/users");  // 1. request
  if (!res.ok) {                                            // 2. check the status
    throw new Error(`HTTP ${res.status}`);
  }
  const users = await res.json();                           // 3. parse the body
  return users;
}
  1. fetch(url) returns a promise for a Response object. await it.
  2. Check res.ok, which is true for status codes 200–299. This step matters because fetch only rejects when the network itself fails (offline, DNS error). A 404 or 500 still resolves normally, so without this check your code would try to use an error page as data.
  3. res.json() reads the body and parses it as JSON. It also returns a promise, so await it too. (Use res.text() for plain text.)

Rendering the results

Once you have an array of objects, turn each one into an element. Use textContent so data from the server is shown as text and can never inject HTML:

function renderUsers(users, listEl) {
  listEl.replaceChildren();          // clear old items
  for (const user of users) {
    const li = document.createElement("li");
    li.textContent = `${user.name} (${user.email})`;
    listEl.append(li);
  }
}

replaceChildren() with no arguments empties an element, so rendering twice does not duplicate the list.

Loading and error states

A request can take a while or fail. A good interface tells the user what is happening:

async function showUsers(listEl, statusEl) {
  statusEl.textContent = "Loading...";
  try {
    const users = await loadUsers();
    renderUsers(users, listEl);
    statusEl.textContent = `Loaded ${users.length} users`;
  } catch (err) {
    statusEl.textContent = "Could not load users. Please try again.";
  }
}

Three states (loading, success, error) are the minimum for any data-driven component. You will see the same pattern in React later.

Sending data

fetch can also send data. Pass an options object with a method, headers and a JSON body:

await fetch("/api/notes", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "My note" })
});

Passing fetch in (for testing)

In the activity, your function receives the fetch function as a parameter, loadUsers(fetchFn, ...). The practice sandbox has no network access, so the page provides a fakeFetch that returns a real Response object with sample data, and the checks can pass in a failing one. This pattern, called dependency injection, is also how professional teams test code that talks to servers without calling the real server.

const fakeFetch = () =>
  Promise.resolve(new Response(JSON.stringify([{ name: "Ada" }]), {
    status: 200,
    headers: { "Content-Type": "application/json" }
  }));

CORS in one paragraph

When you call an API on a different domain from a browser, the server must allow it with CORS headers. If it does not, the browser blocks the response and you see a CORS error in the console, even though the request worked in another tool. That is a server setting, not a bug in your fetch call. Many public APIs allow it; for others, requests go through your own backend (this site's frontend proxies every call through its own /api routes for this and other reasons).

Never put secrets in frontend code

Anything in browser JavaScript can be read by anyone who opens DevTools. API keys that cost money or grant access belong on a server, never in a fetch call in the page.

Common mistakes

  • Forgetting to check res.ok. A 404 is not an exception for fetch.
  • Forgetting the second await. res.json() returns a promise.
  • Using innerHTML with API data. Use textContent.
  • No loading or error state. The page looks broken while waiting or after a failure.
  • Rendering without clearing. Old items pile up; clear the list first.

Quick recap

  • await fetch(url), check res.ok, then await res.json().
  • fetch rejects only on network failure; check HTTP errors yourself.
  • Render with createElement and textContent; show loading, success and error states.
  • Keep secrets on the server.

Try it

Open the Do tab: write loadUsers so it fetches a user list, renders it, and shows a status message (including when the request fails).

Also in this lesson: