Complete JavaScript Course Module 12: Advanced: Asynchronous JavaScript

Fetch API: How to Get Data from an API

  • Beginner
  • Lesson 12.4
  • 6 min read
  • 55 of 76 in the course

In short

The JavaScript Fetch API lets you get data from an API and send data to a server. Learn GET and POST requests, error handling, loading states and timeouts. What is an API and a REST endpoint? An API (Application Programming Interface) is a way for one program to talk to another.

The JavaScript Fetch API lets you get data from an API and send data to a server. Learn GET and POST requests, error handling, loading states and timeouts.

What is an API and a REST endpoint?

An API (Application Programming Interface) is a way for one program to talk to another. On the web, it usually means a server that gives you data when you ask at a certain address. Think of a waiter again: you do not walk into the kitchen; you ask the waiter (the API), and they bring you what you ordered.

A REST endpoint is one URL of that API that gives you one kind of data. For example, https://jsonplaceholder.typicode.com/users returns a list of users, and https://jsonplaceholder.typicode.com/users/1 returns only user number 1. The kind of request you make is called the HTTP method: GET to read data, POST to create, PUT/PATCH to update, and DELETE to remove.

In this lesson we use JSONPlaceholder, a free fake API for practice. It needs no key or account.

How to use fetch in JavaScript (GET request)

fetch(url) sends a request and returns a promise. The promise gives you a Response object. The data itself is still not read yet, so you call response.json(), which returns another promise with the parsed JSON.

async function getUser() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
  console.log(response.status); // Output: 200
  console.log(response.ok);     // Output: true

  const user = await response.json(); // read and parse the body
  console.log(user.name);  // Output: Leanne Graham
  console.log(user.email); // Output: Sincere@april.biz
}

getUser();
  • response.status is the HTTP status code. 200 means OK, 404 means not found, 500 means server error.
  • response.ok is true when the status is between 200 and 299.
  • response.json() turns the JSON text into a normal JavaScript object, just like JSON.parse.

Why fetch does not fail on 404: checking response.ok

This surprises many people: fetch only rejects when the request could not be sent at all, for example when you are offline. A 404 or 500 is still a "successful" response from the network's point of view. So you must check response.ok yourself.

async function getPost(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
    if (!response.ok) {
      throw new Error(`Request failed with status ${response.status}`);
    }
    const post = await response.json();
    console.log("Title:", post.title);
  } catch (error) {
    console.log("Error:", error.message);
  }
}

getPost(1);    // Output: Title: sunt aut facere repellat provident ...
getPost(9999); // Output: Error: Request failed with status 404

Common mistake: Calling response.json() without checking response.ok first. On an error page you may get confusing data or a JSON parse error instead of a clear message.

How to send a POST request with a JSON body

To send data, give fetch a second argument: an options object with the method, headers and body. The body must be a string, so use JSON.stringify. The Content-Type header tells the server you are sending JSON.

async function createPost() {
  const newPost = { title: "My first API post", body: "Hello from fetch!", userId: 1 };

  const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(newPost),
  });

  const saved = await response.json();
  console.log(response.status); // Output: 201  (201 means "Created")
  console.log(saved);           // Output: { title: "My first API post", body: "Hello from fetch!", userId: 1, id: 101 }
}

createPost();

JSONPlaceholder pretends to save the post and sends it back with a new id. It does not really store it, which makes it safe for practice.

How to show loading and error states in the UI

Real users need to see what is happening. A good pattern has three states: loading, success and error. Here is a complete page that loads a user list with a button.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>User list</title>
</head>
<body>
  <button id="load">Load users</button>
  <p id="status"></p>
  <ul id="users"></ul>

  <script>
    const button = document.querySelector("#load");
    const status = document.querySelector("#status");
    const list = document.querySelector("#users");

    button.addEventListener("click", async () => {
      button.disabled = true;           // stop double clicks
      status.textContent = "Loading..."; // loading state
      list.innerHTML = "";

      try {
        const response = await fetch("https://jsonplaceholder.typicode.com/users");
        if (!response.ok) throw new Error(`Status ${response.status}`);
        const users = await response.json();

        users.forEach((user) => {
          const li = document.createElement("li");
          li.textContent = `${user.name} (${user.email})`;
          list.append(li);
        });
        status.textContent = `Loaded ${users.length} users.`; // success state
      } catch (error) {
        status.textContent = `Could not load users: ${error.message}`; // error state
      } finally {
        button.disabled = false;
      }
    });
  </script>
</body>
</html>

When you click the button, you see "Loading...", then a list of 10 names and the message "Loaded 10 users." If you go offline and click again, you see the error message instead. Using textContent (not innerHTML) for API data keeps you safe from injected HTML.

How to add a timeout with AbortController

fetch can wait a long time on a slow network. An AbortController lets you cancel a request. The shortest way to add a timeout is AbortSignal.timeout(ms), which is supported in all modern browsers.

async function getWithTimeout(url, ms) {
  try {
    const response = await fetch(url, { signal: AbortSignal.timeout(ms) });
    if (!response.ok) throw new Error(`Status ${response.status}`);
    return await response.json();
  } catch (error) {
    if (error.name === "TimeoutError") {
      console.log(`Request took longer than ${ms} ms`);
    } else {
      console.log("Error:", error.message);
    }
  }
}

getWithTimeout("https://jsonplaceholder.typicode.com/todos/1", 5000)
  .then((todo) => console.log(todo?.title));
// Output: delectus aut autem

// To cancel by hand (for example, on a "Cancel" button):
const controller = new AbortController();
fetch("https://jsonplaceholder.typicode.com/photos", { signal: controller.signal })
  .catch((error) => console.log(error.name)); // Output: AbortError
controller.abort();

With a manual AbortController, you pass controller.signal to fetch and call controller.abort() whenever you want to stop.

What is CORS?

CORS (Cross-Origin Resource Sharing) is a browser safety rule. By default, a page on one website cannot read data from a different website. The other server must say "this is allowed" by sending special response headers. JSONPlaceholder allows everyone, so it works from any page.

If you see a red "blocked by CORS policy" error, the problem is on the server side, not in your fetch code. You cannot fix it from the browser. The server must allow your site, or you call the API through your own backend.

Tip: Open your browser DevTools and go to the Network tab while you test. You can see every request, its status code and the exact data the server sent back. Learn more on MDN: Using the Fetch API.

Practice exercise

Build a small "Todo viewer" page. Add a number input and a button. When the user clicks, fetch https://jsonplaceholder.typicode.com/todos/{number} and show the title, plus "Done" or "Not done" based on the completed field. Show "Loading..." while waiting and a friendly message if the todo does not exist (try number 500).

Hint: check response.ok and throw an error for a 404, then show it in the catch block.

Key takeaways

  • An API endpoint is a URL that returns data; GET reads and POST creates.
  • await fetch(url) gives a Response; await response.json() gives the data.
  • fetch does not reject on 404 or 500, so always check response.ok.
  • For POST, set method, a Content-Type: application/json header and a JSON.stringify body.
  • Show loading and error states, and use AbortSignal.timeout() to stop slow requests.

Frequently asked questions

What is the difference between fetch and XMLHttpRequest?

XMLHttpRequest is the old way to make requests, based on events and callbacks. fetch is the modern, promise-based replacement. Use fetch in new code.

Can I use fetch without async and await?

Yes. fetch(url).then((res) => res.json()).then((data) => console.log(data)) works the same way. async/await is just easier to read.

Why do I get a CORS error?

The server you are calling does not allow requests from your website. Only the server owner can change this, or you can call the API from your own server instead.