Skip to content
Laviyx Learn

Try

JavaScript course menu

Fetching Data from APIs with JavaScript fetch()

Learn to call APIs with the JavaScript fetch API: GET requests, reading JSON with response.json(), checking response.ok and status codes, error handling, POST requests with JSON bodies and headers, query strings, loading states, AbortController timeouts, and CORS.

Intermediate9 min readUpdated
On this page

An API (application programming interface) lets your code ask a server for data or tell it to do something. On the web, most APIs are called over HTTP and exchange JSON. The built-in fetch function makes these requests, in browsers and in Node.js (version 18 and later).

The examples use JSONPlaceholder (jsonplaceholder.typicode.com), a free public test API that returns the same fake data every time, so the outputs below are real.

A GET request#

JavaScript
const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await response.json();

console.log(response.status);
console.log(user.name);
console.log(user.address.city);
Output
200
Leanne Graham
Gwenborough

Two steps, both asynchronous:

  1. await fetch(url) resolves to a Response once the server's headers arrive.
  2. await response.json() reads the body and parses it as JSON. (response.text() gives the raw text instead.)

Checking for HTTP errors#

fetch only rejects on network failures (no connection, DNS errors, blocked requests). An HTTP error such as 404 Not Found or 500 Server Error still resolves, so you must check response.ok yourself:

JavaScript
const response = await fetch("https://jsonplaceholder.typicode.com/users/9999");
console.log(response.ok, response.status);
Output
false 404

A reusable helper that handles both kinds of failure:

JavaScript
async function getJson(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Request failed: ${response.status} ${response.statusText}`);
  }
  return response.json();
}

try {
  const posts = await getJson("https://jsonplaceholder.typicode.com/posts?userId=1");
  console.log(`${posts.length} posts, first: "${posts[0].title.slice(0, 30)}..."`);
  await getJson("https://jsonplaceholder.typicode.com/nope");
} catch (error) {
  console.log(error.message);
}
Output
10 posts, first: "sunt aut facere repellat provi..."
Request failed: 404 Not Found
StatusMeaning
200 OK, 201 Created, 204 No ContentSuccess
400 Bad Request, 422The data you sent is invalid
401 Unauthorized, 403 ForbiddenNot logged in, or not allowed
404 Not FoundNo such resource
429 Too Many RequestsRate limit reached
500, 502, 503Problem on the server

Query strings#

Build query strings with URLSearchParams, which encodes special characters for you:

JavaScript
const params = new URLSearchParams({ userId: 2, _limit: 3 });
const url = `https://jsonplaceholder.typicode.com/todos?${params}`;
console.log(url);

const todos = await (await fetch(url)).json();
console.log(todos.map((t) => t.id));
Output
https://jsonplaceholder.typicode.com/todos?userId=2&_limit=3
[ 21, 22, 23 ]

Sending data: POST#

To send data, pass an options object with the method, headers and a body:

JavaScript
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "My first post", body: "Hello!", userId: 1 }),
});

const created = await response.json();
console.log(response.status, created);
Output
201 { title: 'My first post', body: 'Hello!', userId: 1, id: 101 }
  • method: "POST", "PUT", "PATCH" or "DELETE" (default "GET").
  • headers: Content-Type: application/json tells the server the body is JSON. Authenticated APIs usually also need an Authorization header.
  • body: a string, so objects go through JSON.stringify. Form data can be sent as new FormData(form) instead.

JSONPlaceholder pretends to save the post (and always gives it id 101) but does not really store it.

Showing data in a page#

In a web page, show a loading state, render the data, and handle errors:

HTML
<button id="load">Load users</button>
<p id="status"></p>
<ul id="users"></ul>
JavaScript
const status = document.querySelector("#status");
const list = document.querySelector("#users");

document.querySelector("#load").addEventListener("click", async () => {
  status.textContent = "Loading...";
  list.replaceChildren();
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users?_limit=5");
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const users = await response.json();
    list.replaceChildren(...users.map((u) => {
      const li = document.createElement("li");
      li.textContent = `${u.name} (${u.email})`;
      return li;
    }));
    status.textContent = `Loaded ${users.length} users`;
  } catch (error) {
    status.textContent = `Could not load users: ${error.message}`;
  }
});
Result

The example uses textContent for API data, never innerHTML, so that data containing HTML cannot inject scripts.

Timeouts with AbortController#

fetch has no built-in timeout, but it can be cancelled with an abort signal. AbortSignal.timeout(ms) creates one that fires automatically:

JavaScript
try {
  await fetch("https://jsonplaceholder.typicode.com/photos", {
    signal: AbortSignal.timeout(1),
  });
} catch (error) {
  console.log(error.name);
}
Output
TimeoutError

With a manual AbortController, you can also cancel a request yourself, for example when the user types a new search before the previous one finishes.

CORS#

A page can only read responses from another origin (domain) if that server allows it with CORS (Cross-Origin Resource Sharing) headers. If not, the browser blocks the response and shows a CORS error in the console, even though the request reached the server. CORS is set on the server; you cannot fix it from front-end code. Node.js does not apply CORS, which is why scripts on a server can call APIs that a browser page cannot.

Keep secrets on the server#

Anything in front-end JavaScript can be read by visitors, including API keys. Keys for paid or private APIs must be used from your server (for example, a Node.js API route), which then calls the external API on the browser's behalf.

Practice#

Practice exercise

Fetch the todos for userId=1 from JSONPlaceholder, then print how many there are, how many are completed, and the titles of the first two that are not completed.

Show solution
JavaScript
const response = await fetch("https://jsonplaceholder.typicode.com/todos?userId=1");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const todos = await response.json();

const done = todos.filter((t) => t.completed);
const pending = todos.filter((t) => !t.completed);

console.log(todos.length, "todos");
console.log(done.length, "completed");
console.log(pending.slice(0, 2).map((t) => t.title));
Output
20 todos
11 completed
[ 'delectus aut autem', 'quis ut nam facilis et officia qui' ]

Key takeaway#

Finished this lesson?