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:
200OK,404not found,500server 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;
}
fetch(url)returns a promise for a Response object.awaitit.- Check
res.ok, which istruefor status codes 200–299. This step matters becausefetchonly rejects when the network itself fails (offline, DNS error). A404or500still resolves normally, so without this check your code would try to use an error page as data. res.json()reads the body and parses it as JSON. It also returns a promise, soawaitit too. (Useres.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 forfetch. - Forgetting the second
await.res.json()returns a promise. - Using
innerHTMLwith API data. UsetextContent. - 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), checkres.ok, thenawait res.json().fetchrejects only on network failure; check HTTP errors yourself.- Render with
createElementandtextContent; 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).