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:
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 code —
200success,404not found,500server 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-Typeheader 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.
A Real Example — GitHub User Search
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.okistruefor status codes 200-299 — always check it before using the datafetch()only rejects on network failures —404and500do not reject automatically- Pass a second options object for non-GET requests — set
method,headers, andbody - Always
JSON.stringify()the body and setContent-Type: application/jsonfor POST/PUT/PATCH - Build a reusable helper to avoid repeating headers, error checks, and JSON parsing everywhere
- Use
try/catcharound fetch calls for clean error handling