DocsHub
Async JavaScript

Fetch API

Learn how to make HTTP requests and load data from APIs using the Fetch API in JavaScript.

Fetch API

Every real web application talks to a server — loading user data, sending form submissions, getting the latest posts, fetching weather information. The Fetch API is the modern built-in way to make HTTP requests in JavaScript.

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

Two lines. That is all it takes to load data from a server. No libraries, no setup — the Fetch API is built into every modern browser and Node.js.


How HTTP Requests Work

Before writing code, a quick mental model of what happens when you fetch data:

HTTP Request HTTP Response Your JavaScript Server

Every request has:

  • A URL — where to send the request
  • A method — what to do — GET, POST, PUT, DELETE
  • Optional headers — extra information like content type or auth token
  • Optional body — data to send with the request

Every response has:

  • A status code200 success, 404 not found, 500 server error
  • Headers — info about the response
  • A body — the actual data returned

Basic GET Request

A GET request fetches data from a server. It is the default method for fetch().

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

getUsers();

fetch() returns a Promise that resolves to a Response object — not the data itself. You then call .json() on the response to parse the body as JSON — which is also a Promise, so you await that too.

The Response object

async function getData() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");

  console.log(response.status);     // 200
  console.log(response.ok);         // true — status is 200-299
  console.log(response.statusText); // "OK"
  console.log(response.headers);    // response headers

  const data = await response.json();
  console.log(data);
}

response.ok is true when the status code is between 200 and 299. This is the simplest way to check if the request succeeded.


Checking for Errors

This is a common mistake with fetch — it only rejects on network failures like no internet connection. A 404 Not Found or 500 Server Error does not reject the Promise. You have to check response.ok yourself.

async function getUser(id) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);

  // ❌ Wrong — does not catch 404, 500 errors
  const data = await response.json();
  return data;
}
async function getUser(id) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);

  // ✅ Correct — check response.ok first
  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }

  const data = await response.json();
  return data;
}

Always check response.ok before parsing the body.


Parsing Response Data

The Response object has several methods to parse the body depending on what the server returns:

const response = await fetch(url);

// JSON data — most common
const data = await response.json();

// Plain text
const text = await response.text();

// Binary data — images, files
const blob = await response.blob();

// Form data
const formData = await response.formData();

For most APIs you will use .json(). Use .text() for plain text responses and .blob() for file downloads.


POST Request — Sending Data

A POST request sends data to the server — creating a new resource. Pass a second argument to fetch() with the request options.

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

  if (!response.ok) {
    throw new Error(`Failed to create user: ${response.status}`);
  }

  const newUser = await response.json();
  return newUser;
}

const result = await createUser({
  name: "Ali Hassan",
  email: "ali@example.com",
  role: "developer"
});

console.log("Created:", result);

Three important things for POST requests:

  • Set method: "POST"
  • Set the Content-Type header to "application/json" so the server knows what format you are sending
  • Convert your data to a JSON string with JSON.stringify()

PUT and PATCH — Updating Data

PUT replaces the entire resource. PATCH updates only specific fields.

// PUT — replace entire user
async function updateUser(id, userData) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(userData)
  });

  if (!response.ok) throw new Error(`Update failed: ${response.status}`);
  return response.json();
}

// PATCH — update only specific fields
async function patchUser(id, changes) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(changes)
  });

  if (!response.ok) throw new Error(`Patch failed: ${response.status}`);
  return response.json();
}

// Update just the email
await patchUser(1, { email: "newemail@example.com" });

DELETE Request

async function deleteUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: "DELETE"
  });

  if (!response.ok) throw new Error(`Delete failed: ${response.status}`);

  console.log(`User ${id} deleted.`);
}

await deleteUser(1);

Sending Headers

Headers pass extra information with the request — authentication tokens, content types, API keys.

async function getProtectedData(token) {
  const response = await fetch("https://api.example.com/protected", {
    method: "GET",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json",
      "Accept": "application/json"
    }
  });

  if (!response.ok) {
    if (response.status === 401) {
      throw new Error("Unauthorized — please log in.");
    }
    throw new Error(`Request failed: ${response.status}`);
  }

  return response.json();
}

Building a Reusable API Helper

In real projects you do not write raw fetch() everywhere. You build a helper function that handles the repetitive parts — setting headers, checking response.ok, parsing JSON, handling errors.

async function request(url, options = {}) {
  const defaultHeaders = {
    "Content-Type": "application/json",
    "Accept": "application/json"
  };

  const config = {
    ...options,
    headers: {
      ...defaultHeaders,
      ...options.headers
    }
  };

  if (config.body && typeof config.body === "object") {
    config.body = JSON.stringify(config.body);
  }

  const response = await fetch(url, config);

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(error.message ?? `Request failed: ${response.status}`);
  }

  // Handle empty responses (DELETE, 204 No Content)
  const text = await response.text();
  return text ? JSON.parse(text) : null;
}

// Clean usage
const api = {
  get: (url, options) => request(url, { ...options, method: "GET" }),
  post: (url, body, options) => request(url, { ...options, method: "POST", body }),
  put: (url, body, options) => request(url, { ...options, method: "PUT", body }),
  patch: (url, body, options) => request(url, { ...options, method: "PATCH", body }),
  delete: (url, options) => request(url, { ...options, method: "DELETE" })
};

// Usage
const users = await api.get("https://api.example.com/users");
const newUser = await api.post("https://api.example.com/users", { name: "Ali" });
await api.delete("https://api.example.com/users/1");

One helper handles all methods. Headers are set automatically. Errors are thrown consistently. JSON is parsed automatically.


async function searchGitHubUser(username) {
  const resultEl = document.getElementById("result");
  const loadingEl = document.getElementById("loading");

  loadingEl.style.display = "block";
  resultEl.innerHTML = "";

  try {
    const response = await fetch(`https://api.github.com/users/${username}`);

    if (response.status === 404) {
      throw new Error(`User "${username}" not found.`);
    }

    if (!response.ok) {
      throw new Error(`GitHub API error: ${response.status}`);
    }

    const user = await response.json();

    resultEl.innerHTML = `
      <div class="user-card">
        <img src="${user.avatar_url}" alt="${user.login}" width="80">
        <h2>${user.name ?? user.login}</h2>
        <p>${user.bio ?? "No bio."}</p>
        <p>📍 ${user.location ?? "Unknown location"}</p>
        <p>
          ⭐ ${user.public_repos} repos ·
          👥 ${user.followers} followers ·
          ${user.following} following
        </p>
        <a href="${user.html_url}" target="_blank">View on GitHub →</a>
      </div>
    `;

  } catch (error) {
    resultEl.innerHTML = `<p class="error">${error.message}</p>`;
  } finally {
    loadingEl.style.display = "none";
  }
}

document.getElementById("search-btn").addEventListener("click", () => {
  const username = document.getElementById("username-input").value.trim();
  if (username) searchGitHubUser(username);
});

A real API — GitHub's public API requires no authentication for basic user lookups. Type a GitHub username, hit search, and get a real user card. Handles 404 separately from other errors for a better user experience.

The GitHub API used here is completely public and free — no API key needed. Try it with any real GitHub username like "torvalds" or "gaearon". Perfect for practicing fetch.


Summary

  • fetch(url) makes an HTTP GET request and returns a Promise resolving to a Response object
  • Always await response.json() to parse the response body — it is also a Promise
  • response.ok is true for status codes 200-299 — always check it before using the data
  • fetch() only rejects on network failures — 404 and 500 do not reject automatically
  • Pass a second options object for non-GET requests — set method, headers, and body
  • Always JSON.stringify() the body and set Content-Type: application/json for POST/PUT/PATCH
  • Build a reusable helper to avoid repeating headers, error checks, and JSON parsing everywhere
  • Use try/catch around fetch calls for clean error handling

On this page