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:
| Method | Returns | Use Case |
|---|---|---|
response.json() | Parsed JSON | Most APIs return JSON |
response.text() | Plain string | HTML, CSV, text data |
response.blob() | Binary Blob | Images, files |
response.formData() | FormData object | Form submissions |
response.arrayBuffer() | ArrayBuffer | Raw binary data |
// Text responseconst html = await fetch("/page.html").then((r) => r.text());
// Binary imageconst blob = await fetch("/image.png").then((r) => r.blob());const imgUrl = URL.createObjectURL(blob);
// Check content type and choose methodconst 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 headerconsole.log(response.headers.get("set-cookie")); // cookies
// Iterate all headersfor (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 cancellationconst 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 setTimeoutconst 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
| Method | Purpose | Body? | Idempotent? |
|---|---|---|---|
GET | Retrieve data | No | Yes |
POST | Create resource | Yes | No |
PUT | Replace resource | Yes | Yes |
PATCH | Partial update | Yes | No |
DELETE | Remove resource | No | Yes |
| Response Property | Description |
|---|---|
response.ok | true if status 200-299 |
response.status | HTTP status code (200, 404, etc.) |
response.statusText | Status text (“OK”, “Not Found”) |
response.headers | Headers object |
response.url | Final URL after redirects |
response.redirected | Was the request redirected? |
Practice Exercises
Build an API client: Create a reusable
ApiClientclass that handles all HTTP methods, sets auth headers automatically, and provides consistent error handling.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).Upload with progress: Research how to track upload progress (hint:
XMLHttpRequeststill wins here), or implement a download indicator usingresponse.body.getReader()with a ReadableStream.