Skip to main content

Skillber v1.0 is here!

Learn more

The Fetch API & HTTP

Checking access...

The Fetch API provides a modern, Promise-based interface for making HTTP requests. It replaces the older XMLHttpRequest with a cleaner, more flexible API.

Basic GET Request

fetch("https://api.example.com/users/1")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(data); // parsed JSON object
})
.catch((error) => {
console.error("Fetch failed:", error);
});

With async/await:

async function getUser(id) {
try {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Failed to fetch user:", error);
throw error;
}
}
const user = await getUser(1);
console.log(user.name);

Caution

fetch() only rejects on network errors (no internet, DNS failure). An HTTP 404 or 500 does not cause a rejection — you must check response.ok or response.status yourself.

Response Methods

The Response object provides several methods to read the body:

MethodReturnsUse Case
response.json()Parsed JSONMost APIs return JSON
response.text()Plain stringHTML, CSV, text data
response.blob()Binary BlobImages, files
response.formData()FormData objectForm submissions
response.arrayBuffer()ArrayBufferRaw binary data
// Text response
const html = await fetch("/page.html").then((r) => r.text());
// Binary image
const blob = await fetch("/image.png").then((r) => r.blob());
const imgUrl = URL.createObjectURL(blob);
// Check content type and choose method
const response = await fetch(url);
if (response.headers.get("content-type")?.includes("application/json")) {
return response.json();
} else {
return response.text();
}

Request Headers

Setting Request Headers

fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIs...",
"X-Request-ID": "abc-123",
},
body: JSON.stringify({ name: "Alice", email: "alice@example.com" }),
});

Reading Response Headers

const response = await fetch("https://api.example.com/data");
console.log(response.headers.get("content-type")); // "application/json"
console.log(response.headers.get("x-request-id")); // custom header
console.log(response.headers.get("set-cookie")); // cookies
// Iterate all headers
for (const [key, value] of response.headers) {
console.log(`${key}: ${value}`);
}

Common HTTP Methods

POST — Create a Resource

async function createUser(userData) {
const response = await fetch("https://api.example.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `Failed to create user (${response.status})`);
}
return response.json();
}

PUT — Update a Resource

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 — Partial Update

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

DELETE — Remove a Resource

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}`);
}
// DELETE often returns 204 No Content (no body)
return response.status === 204 ? null : response.json();
}

Error Handling Patterns

Centralised HTTP Client

class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
headers: {
"Content-Type": "application/json",
...options.headers,
},
...options,
};
try {
const response = await fetch(url, config);
const data = response.status === 204 ? null : await response.json();
if (!response.ok) {
const error = new Error(data?.message || `HTTP ${response.status}`);
error.status = response.status;
error.data = data;
throw error;
}
return data;
} catch (error) {
if (error.name === "TypeError" && error.message === "Failed to fetch") {
throw new Error("Network error — check your connection");
}
throw error;
}
}
get(endpoint, options) {
return this.request(endpoint, { ...options, method: "GET" });
}
post(endpoint, body, options) {
return this.request(endpoint, {
...options,
method: "POST",
body: JSON.stringify(body),
});
}
put(endpoint, body, options) {
return this.request(endpoint, {
...options,
method: "PUT",
body: JSON.stringify(body),
});
}
delete(endpoint, options) {
return this.request(endpoint, { ...options, method: "DELETE" });
}
}
const api = new ApiClient("https://api.example.com");
// Usage:
const user = await api.get("/users/1");
const newUser = await api.post("/users", { name: "Alice" });

Status Code Handling

async function handleResponse(response) {
switch (response.status) {
case 200:
case 201:
return response.json();
case 204:
return null;
case 400:
throw { code: "VALIDATION_ERROR", detail: await response.json() };
case 401:
throw { code: "UNAUTHORIZED", message: "Please log in again" };
case 403:
throw { code: "FORBIDDEN", message: "You don't have permission" };
case 404:
throw { code: "NOT_FOUND", message: "Resource not found" };
case 429:
throw { code: "RATE_LIMITED", message: "Too many requests" };
case 500:
case 502:
case 503:
throw { code: "SERVER_ERROR", message: "Server is unavailable" };
default:
throw { code: "UNKNOWN", message: `Unexpected status ${response.status}` };
}
}

Request Options Reference

const response = await fetch(url, {
method: "GET", // HTTP method
headers: { // Request headers
"Content-Type": "application/json",
"Authorization": "Bearer token",
},
body: JSON.stringify(data), // Request body (POST/PUT/PATCH)
mode: "cors", // cors, no-cors, same-origin
credentials: "include", // omit, same-origin, include
cache: "no-cache", // default, no-cache, reload, force-cache
redirect: "follow", // follow, error, manual
referrerPolicy: "no-referrer", // referrer policy
signal: AbortSignal.timeout(5000), // timeout (ES2022+)
});

AbortController (Request Cancellation)

Cancel in-flight requests using AbortController:

function searchUsers(query) {
const controller = new AbortController();
// Cancel previous request if new one comes in
if (searchUsers.currentController) {
searchUsers.currentController.abort();
}
searchUsers.currentController = controller;
return fetch(`/api/users?q=${query}`, {
signal: controller.signal,
}).then((r) => r.json());
}
// Manual cancellation
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // timeout
try {
const data = await fetch("/api/slow-endpoint", {
signal: controller.signal,
});
} catch (err) {
if (err.name === "AbortError") {
console.log("Request was cancelled");
}
}

With timeout (ES2022+):

// Built-in timeout — equivalent to AbortController with setTimeout
const response = await fetch(url, {
signal: AbortSignal.timeout(10000), // 10 second timeout
});

Sending Form Data

JSON Body

fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice", email: "alice@example.com" }),
});

URL-Encoded Form

fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
username: "alice",
password: "secret123",
}),
});

Multipart Form (File Upload)

const formData = new FormData();
formData.append("name", "Avatar");
formData.append("file", fileInput.files[0]); // File object
fetch("/api/upload", {
method: "POST",
// Do NOT set Content-Type — browser will set it with boundary
body: formData,
});

Loading States & UX Patterns

async function fetchWithLoading(url) {
// Show loading state
showSpinner();
try {
const data = await fetch(url).then((r) => r.json());
return data;
} catch (error) {
showError(error.message);
throw error;
} finally {
hideSpinner();
}
}

Race Condition Prevention

async function fetchLatest(query) {
const requestId = Date.now();
fetchLatest.lastId = requestId;
const results = await searchApi(query);
// Only update UI if this is still the latest request
if (fetchLatest.lastId === requestId) {
renderResults(results);
}
}

Quick Reference

MethodPurposeBody?Idempotent?
GETRetrieve dataNoYes
POSTCreate resourceYesNo
PUTReplace resourceYesYes
PATCHPartial updateYesNo
DELETERemove resourceNoYes
Response PropertyDescription
response.oktrue if status 200-299
response.statusHTTP status code (200, 404, etc.)
response.statusTextStatus text (“OK”, “Not Found”)
response.headersHeaders object
response.urlFinal URL after redirects
response.redirectedWas the request redirected?

Practice Exercises

  1. Build an API client: Create a reusable ApiClient class that handles all HTTP methods, sets auth headers automatically, and provides consistent error handling.

  2. Country info app: Fetch from https://restcountries.com/v3.1/name/{country} and display the country’s capital, population, flag, and languages. Handle errors gracefully (invalid country, network issues).

  3. Upload with progress: Research how to track upload progress (hint: XMLHttpRequest still wins here), or implement a download indicator using response.body.getReader() with a ReadableStream.