Skip to content
wannacode

Data from an API

In short: fetch gets data from an API with async/await, checks response.ok and reads the JSON, and the page shows one of four states: loading, error, empty or data.

Problem

An empty shop window

The dairy page takes its products from the server. Today the server has a problem and answers with error 500. The buyer sees a heading and nothing under it.

HTML, CSS, JS

The console says products.map is not a function. The code took the answer of the error, { "error": "…" }, for a list of products. The buyer sees no error and no "try again". And even when the server works, the page is blank while the answer is on its way.

The idea

fetch on the page

fetch(address) sends a request and returns a promise (Promise) of the response. With await the function waits for the response without stopping the page:

const response = await fetch('/api/products?category=4');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
  • fetch throws only when there is no answer: no network. A response with 404 or 500 is an answer, and the promise is fulfilled.
  • response.ok is true only for codes from 200 to 299. Checking it is your job.
  • response.json() reads the body and turns it into an object. This is a promise too, so it also gets await.

In the sandbox the addresses /api/… are answered by the shop's pretend API, with no network: GET /api/products (with ?q= and ?category=), /api/products/3, /api/categories, /api/orders and POST /api/orders. A lesson can make it slow, tell it to answer with 500 or make it return an empty list. This way every state is real and always the same.

The idea

Four states

A request takes time and has an outcome. The screen has to say what is going on at every moment:

  • loading: the request has left, there is no answer;
  • data: a list of products has come;
  • empty: a list has come, but it is empty. This is not an error;
  • error: there is no answer, or the answer is not ok.

Every state is an element in the HTML. The code shows and hides the elements with the hidden property:

<p id="loading" role="status">Loading…</p>
<p id="error" role="alert" hidden></p>
<p id="empty" hidden>There are no products in this category.</p>
<ul id="products"></ul>

"Loading…" is visible from the very start, before JavaScript even runs. role="status" makes a screen reader read it, and role="alert" reads the error as soon as it appears.

See it

Watch one request

async function load() {
try {
const response = await fetch('/api/products?category=4');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
empty.hidden = products.length > 0;
list.replaceChildren(...products.map(productItem));
} catch {
error.textContent = 'The products did not load.';
error.hidden = false;
} finally {
loading.hidden = true;
}
}
Step by step, in words
  1. load() sends the request. There is no answer yet: the screen shows "Loading…" from the HTML.
  2. await waits, but the page does not freeze: the buyer can scroll and the spinner turns. On a slow network this takes seconds.
  3. 200 with an array: ok is true, json() gives the products, the lines are drawn, and finally hides "Loading…".
  4. The same request, answer 500. fetch does not throw: it got an answer. Our check of ok throws, and catch shows the error.
  5. 200 with []: this is not an error. The array is empty, so the page says "no products" instead of showing a blank space.
  6. Four states, each visible: loading, data, error, empty. finally hides "Loading…" on every way out of try.

The same code, three different answers. With 500 fetch does not throw: the check of ok throws, and catch shows the error. finally hides "Loading…" in all three cases.

See it

The slow API

Here the API answers after 1.2 seconds. Press "Run" and watch "Loading…".

HTML, CSS, JS

The whole loading code:

async function load() {
try {
const response = await fetch('/api/products?category=4');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
empty.hidden = products.length > 0;
list.replaceChildren(...products.map(productItem));
} catch {
error.textContent = 'The products did not load.';
error.hidden = false;
} finally {
loading.hidden = true;
}
}
load();

Try it

Error

Here the first request gets a 500 and the second one goes through. On an error, make the page show "The products did not load." in #error and the "Try again" button, make "Loading…" disappear, and do not show "no products". The button already loads again: after it the products must be on the page.

HTML, CSS, JS

Try it

Empty

Here the API returns an empty list. Show "There are no products in this category." when there are zero products, and hide it when there are some.

HTML, CSS, JS

Try it

Find the bug

The server answers with 200 and products, and the page shows an error.

Pick the line with the bug.

The idea

Sending: the button and the wait

The order leaves with POST. The data are in the body as JSON, and the Content-Type header tells the server what it gets:

const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...data, items }),
});

data is the object from the fields of the form: Object.fromEntries(new FormData(form)) turns the name=value pairs into an object { name: …, email: …, address: … }.

While the answer is on its way, the "Place order" button is still enabled. An impatient buyer presses twice:

HTML, CSS, JS

Two requests, two orders: 61 and 62. The page shows the last one, and nobody notices that the first one was accepted too.

Try it

One click, one order

Disable the "Place order" button before the request leaves, and enable it again when the answer comes, even if it is an error. The check presses twice quickly and expects one order: 61.

HTML, CSS, JS
More precisely, if you are curious

A network error. With no network fetch throws TypeError: Failed to fetch. catch catches that and your own throw on !response.ok: for the buyer both mean "did not load".

The body of an error. A server often explains the error in the body, as JSON. On 400 the pretend API returns { "errors": { "email": "…" } }: an error per field. That is why the exercise reads the body on an error too.

Try again. A "Try again" button next to the error just calls load() again. Before that, "Loading…" has to show again and the error has to hide.

A race between requests. If the buyer changes the category twice quickly, the two answers may come in reverse order and the page shows the old category. How to stop an old request (AbortController) is in the module on effects.

Checks in the browser and on the server. required and type="email" stop the form before it leaves. The server checks again, because a request can come without our page. In the exercise the fields have no built-in checks, so you can see what the server answers.

Exercise

The order

Send the order: POST /api/orders with name, email and address from the form and items from the array. While it is being sent, the button is disabled. On 400 put every message from errors into the paragraph under its field (#email-error and so on), and clear the old messages on every new send. On success write "Order 61 is accepted. Total: €4.00" in #result (the number and the total come from the answer).

HTML, CSS, JS

Test

Check yourself

1. The server answers with 500. What does await fetch(…) do?
2. The API returns []. Which condition shows "no products"?
3. Why is the "Place order" button disabled before the request leaves, and not after it?
4. Why is "Loading…" hidden in finally?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·