Modern websites constantly pull in data without reloading the page — a social feed refreshing, search results appearing as you type, a weather app updating. Under almost all of that sits the same tool: fetch.

A basic GET request

fetch("https://api.example.com/users")
  .then(response => response.json())
  .then(data => console.log(data));

fetch sends a request and gives you back a Promise, which is why this pairs naturally with everything from the promises tutorial. The first .then() converts the raw response into usable JSON; the second one actually does something with that data.

The same thing with async/await

async function getUsers() {
  const response = await fetch("https://api.example.com/users");
  const data = await response.json();
  console.log(data);
}

Sending data, not just requesting it

fetch("https://api.example.com/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Ann", age: 30 }),
})
  .then(response => response.json())
  .then(data => console.log("Created:", data));

By default fetch sends a GET request. To send data, you switch the method to POST, tell the server what kind of data is coming with the Content-Type header, and hand over the actual data as a JSON string, using JSON.stringify to convert your JavaScript object into text the server can understand.

Checking whether the request actually succeeded

Here's something that catches a lot of people off guard: fetch doesn't treat a 404 or 500 error as a failure the way you might expect. The Promise still resolves successfully; you've to check response.ok yourself:

const response = await fetch("https://api.example.com/users/999");
if (!response.ok) {
  throw new Error(`Request failed with status ${response.status}`);
}

Handling network failures

try {
  const response = await fetch("https://api.example.com/users");
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.log("Network error:", error);
}

A genuine network failure, like no internet connection at all, does reject the Promise and land in catch. Combine that with the response.ok check above and you're covering both the "request never even reached the server" case and the "server responded, but with an error" case.

Putting it together

fetch, combined with async/await and a bit of error checking, is the backbone of essentially every modern web app that talks to a backend. Once this pattern feels natural, connecting a front end you've built to a real API, including one built with the Spring Boot tutorials elsewhere on this site, stops feeling like a separate skill and starts feeling like the same handful of ideas applied again.