← Back to course

Fetch API

Fetch API

Welcome back.

In the previous lesson, you learned local storage.

Local storage lets JavaScript remember small pieces of data in the browser.

Very useful.

Very practical.

Very “the browser has a tiny notebook now.”

Today we teach JavaScript how to get data from outside the page.

This is where things become very real.

Websites rarely keep all data inside one JavaScript file.

They often load data from:

To get that data, JavaScript uses the Fetch API.

Fetch lets JavaScript ask for data.

The browser says:

I will go and get it.

JavaScript says:

Good. Please do not return chaos.

Sometimes it returns data.

Sometimes it returns an error.

Sometimes it returns something that makes you question your career choices.

Welcome to web development.

What You Will Learn

In this lesson, you will learn:

By the end of this lesson, your page will load data from a JSON file and display it on the screen.

This is a huge step.

Until now, most of our data lived directly inside JavaScript.

Now JavaScript will start getting data from outside.

Like a frontend messenger.

With better shoes.

What Is Fetch?

fetch() is a JavaScript function used to request data.

Example:

fetch("products.json");

This asks the browser to load a file named products.json.

But there is something important.

fetch() does not give the data immediately.

It takes time.

Even if the file is small.

Even if the file is local.

Even if you ask very politely.

So JavaScript treats fetch() as asynchronous.

That means:

Start the request now.
Continue when the result is ready.

This is important.

Very important.

The internet is not instant.

Even when it pretends to be.

What Is JSON?

JSON means JavaScript Object Notation.

It is a text format used to store and send data.

It looks similar to JavaScript objects.

Example JSON:

{
  "name": "JavaScript Course",
  "price": 49,
  "available": true
}

JSON is used everywhere.

APIs use JSON.

Backends return JSON.

Frontend apps read JSON.

Your future Java Spring Boot backend will probably send JSON.

Your frontend will probably drink it like coffee.

Carefully.

Because bad JSON causes sadness.

Create the Project

Create a folder for this lesson:

mkdir javascript-lesson11
cd javascript-lesson11
touch index.html
touch script.js
touch products.json

Your project should look like this:

javascript-lesson11/
  index.html
  script.js
  products.json

Important:

For this lesson, do not open index.html directly with a double click.

Because fetch() may not load local files correctly from file://.

Use a local server.

For example, with Caddy:

caddy file-server --listen :8080

Then open:

http://localhost:8080

Now the browser can load your JSON file properly.

No framework dragon.

Just a small local server.

Civilized.

Write the JSON File

Open products.json and add:

[
  {
    "id": 1,
    "name": "JavaScript Course",
    "price": 49,
    "available": true
  },
  {
    "id": 2,
    "name": "HTML Course",
    "price": 29,
    "available": true
  },
  {
    "id": 3,
    "name": "CSS Course",
    "price": 39,
    "available": false
  }
]

This is an array of objects.

Each object represents one product.

Each product has:

This is very common.

A list of products?

Array of objects.

A list of users?

Array of objects.

A list of blog posts?

Array of objects.

JavaScript sees arrays of objects everywhere.

Like pigeons in a city square.

Write the HTML

Open index.html and add:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Fetch API</title>
</head>
<body>
  <h1>Fetch API</h1>

  <button id="loadButton">Load Products</button>

  <p id="message">Click the button to load products.</p>

  <ul id="productList"></ul>

  <script src="script.js"></script>
</body>
</html>

We have:

Simple.

Clean.

Ready to receive data.

The page is waiting.

Like a waiter with an empty tray.

Your First Fetch

Open script.js and add:

const loadButton = document.getElementById("loadButton");
const messageElement = document.getElementById("message");
const productListElement = document.getElementById("productList");

async function loadProducts() {
  const response = await fetch("products.json");
  const products = await response.json();

  console.log(products);
}

loadButton.addEventListener("click", loadProducts);

Start your local server:

caddy file-server --listen :8080

Open:

http://localhost:8080

Click the button.

Open the console.

You should see the products array.

Congratulations.

JavaScript loaded data from a JSON file.

This is real.

Small real.

But real.

Understanding async and await

Look at this function:

async function loadProducts() {
  const response = await fetch("products.json");
  const products = await response.json();

  console.log(products);
}

The word async means:

This function does asynchronous work.

The word await means:

Wait for this operation to finish before continuing.

This line:

const response = await fetch("products.json");

waits for the file to load.

This line:

const products = await response.json();

waits for the response to be converted into JavaScript data.

Without await, JavaScript does not wait.

And then your code becomes confused.

Like trying to eat pasta before it is cooked.

Technically possible.

Emotionally wrong.

What Is response?

fetch() gives us a response object.

Example:

const response = await fetch("products.json");

The response contains information about the request.

It is not the final data yet.

To get JSON data, we use:

const products = await response.json();

This converts the response body into JavaScript data.

Important:

response.json() is also asynchronous.

That is why we use await.

JavaScript needs time to read and parse the JSON.

Even JavaScript needs a moment.

Respect the process.

Display Products on the Page

Now let us show products in the DOM.

Update script.js:

const loadButton = document.getElementById("loadButton");
const messageElement = document.getElementById("message");
const productListElement = document.getElementById("productList");

async function loadProducts() {
  const response = await fetch("products.json");
  const products = await response.json();

  productListElement.innerHTML = "";

  for (const product of products) {
    const listItem = document.createElement("li");
    listItem.textContent = `${product.name} - €${product.price}`;
    productListElement.appendChild(listItem);
  }

  messageElement.textContent = `${products.length} products loaded.`;
}

loadButton.addEventListener("click", loadProducts);

Refresh the browser.

Click the button.

Now products appear on the page.

What happened?

JavaScript:

This is frontend development.

Data comes in.

DOM updates.

User sees content.

Beautiful.

Slightly dramatic.

But beautiful.

Add Availability Status

Now let us show whether a product is available.

Update this line:

listItem.textContent = `${product.name} - €${product.price}`;

to this:

const status = product.available ? "Available" : "Not available";
listItem.textContent = `${product.name} - €${product.price} - ${status}`;

Full loop:

for (const product of products) {
  const listItem = document.createElement("li");
  const status = product.available ? "Available" : "Not available";

  listItem.textContent = `${product.name} - €${product.price} - ${status}`;
  productListElement.appendChild(listItem);
}

This uses a ternary operator.

It means:

If product.available is true, use "Available".
Otherwise, use "Not available".

Short.

Useful.

A little suspicious at first.

But friendly after practice.

Add Loading State

Fetching data takes time.

So we should show a loading message.

Update loadProducts:

async function loadProducts() {
  messageElement.textContent = "Loading products...";
  productListElement.innerHTML = "";

  const response = await fetch("products.json");
  const products = await response.json();

  for (const product of products) {
    const listItem = document.createElement("li");
    const status = product.available ? "Available" : "Not available";

    listItem.textContent = `${product.name} - €${product.price} - ${status}`;
    productListElement.appendChild(listItem);
  }

  messageElement.textContent = `${products.length} products loaded.`;
}

Now when the user clicks, the page says:

Loading products...

This is good user experience.

Users like knowing something is happening.

Otherwise they click again.

And again.

And again.

Then your app receives 47 requests and starts crying.

Handle Errors with try and catch

Sometimes fetching fails.

Maybe the file name is wrong.

Maybe the server is not running.

Maybe the internet is having a philosophical crisis.

We need to handle errors.

Update script.js:

const loadButton = document.getElementById("loadButton");
const messageElement = document.getElementById("message");
const productListElement = document.getElementById("productList");

async function loadProducts() {
  messageElement.textContent = "Loading products...";
  productListElement.innerHTML = "";

  try {
    const response = await fetch("products.json");
    const products = await response.json();

    for (const product of products) {
      const listItem = document.createElement("li");
      const status = product.available ? "Available" : "Not available";

      listItem.textContent = `${product.name} - €${product.price} - ${status}`;
      productListElement.appendChild(listItem);
    }

    messageElement.textContent = `${products.length} products loaded.`;
  } catch (error) {
    messageElement.textContent = "Could not load products.";
    console.log(error);
  }
}

loadButton.addEventListener("click", loadProducts);

Now errors will not silently destroy the mood.

If something goes wrong, the user sees a message.

And the developer sees the error in the console.

Everyone gets information.

Very healthy.

Very rare.

Check response.ok

There is another important detail.

fetch() does not always throw an error for bad HTTP responses.

For example, if the file is missing, the server may return 404.

So we should check:

if (!response.ok) {
  throw new Error("Failed to load products.");
}

Update the fetch part:

const response = await fetch("products.json");

if (!response.ok) {
  throw new Error("Failed to load products.");
}

const products = await response.json();

Full function:

async function loadProducts() {
  messageElement.textContent = "Loading products...";
  productListElement.innerHTML = "";

  try {
    const response = await fetch("products.json");

    if (!response.ok) {
      throw new Error("Failed to load products.");
    }

    const products = await response.json();

    for (const product of products) {
      const listItem = document.createElement("li");
      const status = product.available ? "Available" : "Not available";

      listItem.textContent = `${product.name} - €${product.price} - ${status}`;
      productListElement.appendChild(listItem);
    }

    messageElement.textContent = `${products.length} products loaded.`;
  } catch (error) {
    messageElement.textContent = "Could not load products.";
    console.log(error);
  }
}

Now the code is stronger.

Not invincible.

But stronger.

Like coffee after the second cup.

Add Better Styles

Let us make the page look nicer.

Update index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Fetch API</title>

  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 60px auto;
      padding: 0 24px;
      background-color: #f3f4f6;
      color: #111827;
    }

    .card {
      background-color: white;
      padding: 24px;
      border: 2px solid #e5e7eb;
      border-radius: 18px;
    }

    h1 {
      font-size: 42px;
    }

    button {
      background-color: #2563eb;
      color: white;
      border: none;
      padding: 14px 20px;
      border-radius: 999px;
      font-weight: 700;
      cursor: pointer;
    }

    button:hover {
      background-color: #1d4ed8;
    }

    .message {
      margin-top: 20px;
      font-size: 18px;
      font-weight: 700;
    }

    .products {
      list-style: none;
      padding: 0;
      margin-top: 20px;
      display: grid;
      gap: 12px;
    }

    .product {
      padding: 16px;
      border: 2px solid #e5e7eb;
      border-radius: 14px;
      background-color: #f9fafb;
    }

    .product strong {
      display: block;
      font-size: 22px;
      margin-bottom: 6px;
    }

    .available {
      color: #166534;
      font-weight: 700;
    }

    .unavailable {
      color: #991b1b;
      font-weight: 700;
    }
  </style>
</head>
<body>
  <h1>Fetch API</h1>

  <div class="card">
    <button id="loadButton">Load Products</button>

    <p id="message" class="message">Click the button to load products.</p>

    <ul id="productList" class="products"></ul>
  </div>

  <script src="script.js"></script>
</body>
</html>

Now update the product display in script.js:

for (const product of products) {
  const listItem = document.createElement("li");
  const status = product.available ? "Available" : "Not available";
  const statusClass = product.available ? "available" : "unavailable";

  listItem.className = "product";

  listItem.innerHTML = `
    <strong>${product.name}</strong>
    <span>Price: €${product.price}</span><br>
    <span class="${statusClass}">${status}</span>
  `;

  productListElement.appendChild(listItem);
}

Now the products look like cards.

Much better.

Less “raw HTML from a cave.”

More “beginner frontend app.”

Progress.

Full JavaScript Code

Here is the full script.js:

const loadButton = document.getElementById("loadButton");
const messageElement = document.getElementById("message");
const productListElement = document.getElementById("productList");

async function loadProducts() {
  messageElement.textContent = "Loading products...";
  productListElement.innerHTML = "";

  try {
    const response = await fetch("products.json");

    if (!response.ok) {
      throw new Error("Failed to load products.");
    }

    const products = await response.json();

    for (const product of products) {
      const listItem = document.createElement("li");
      const status = product.available ? "Available" : "Not available";
      const statusClass = product.available ? "available" : "unavailable";

      listItem.className = "product";

      listItem.innerHTML = `
        <strong>${product.name}</strong>
        <span>Price: €${product.price}</span><br>
        <span class="${statusClass}">${status}</span>
      `;

      productListElement.appendChild(listItem);
    }

    messageElement.textContent = `${products.length} products loaded.`;
  } catch (error) {
    messageElement.textContent = "Could not load products.";
    console.log(error);
  }
}

loadButton.addEventListener("click", loadProducts);

Now you have:

This is a serious beginner project.

Tiny.

But serious.

Like a small espresso.

Common Mistakes

Opening index.html Directly

Wrong:

file:///home/user/javascript-lesson11/index.html

Better:

caddy file-server --listen :8080

Then open:

http://localhost:8080

Fetch often needs a server.

Even a tiny local one.

Do not fight the browser.

It has rules.

Many rules.

Forgetting await

Wrong:

const response = fetch("products.json");
const products = response.json();

Correct:

const response = await fetch("products.json");
const products = await response.json();

Without await, you do not get the data yet.

You get a promise.

A promise is not the result.

It is a future result.

Like ordering pizza.

The receipt is not the pizza.

Again with pizza.

But it works.

Forgetting response.json()

Wrong:

const products = await response;

Correct:

const products = await response.json();

The response is not the final data.

You must convert it to JSON.

JavaScript is powerful.

But it still needs instructions.

Wrong File Path

If your file is named:

products.json

then fetch:

fetch("products.json")

If your file is in a folder:

data/products.json

then fetch:

fetch("data/products.json")

Paths must match.

JavaScript will not search your folders like a detective.

It will just fail.

Quietly.

Like a cat knocking something off a table.

Practice

Create a new JSON file called students.json.

Add this:

[
  {
    "name": "Anna",
    "level": "Beginner"
  },
  {
    "name": "Marco",
    "level": "Intermediate"
  },
  {
    "name": "Viktor",
    "level": "Beginner"
  }
]

Create a button that loads students.

Show each student on the page.

Example output:

Anna - Beginner
Marco - Intermediate
Viktor - Beginner

Bonus:

Show how many students were loaded.

Very similar to products.

Different data.

Same pattern.

This is how programming works.

Learn one pattern.

Use it everywhere.

Pretend it was hard.

Very professional.

Mini Challenge

Build a small course catalog.

Create courses.json with:

Then use fetch() to load the courses and show them as cards.

Each card should show:

Bonus:

Only show available courses.

This means you can use:

if (course.available) {
  // show course
}

This challenge combines:

Basically, JavaScript is now becoming a small web app factory.

Congratulations.

And also: be careful.

Factories need organization.

Summary

Today you learned:

This is a huge step.

Your JavaScript can now talk to external data.

Not just variables.

Not just local storage.

Real files.

APIs next.

Servers next.

The outside world.

JavaScript has opened a window.

Hopefully not the wrong one.

Next Lesson

In the next lesson, we will build a final small project.

We will combine:

A real beginner JavaScript project.

Not huge.

Not scary.

But complete.

The final boss is approaching.

Bring coffee.