Fetch API & HTTP Requests
Introduction to the Fetch API
The Fetch API is a modern, built-in JavaScript interface for making HTTP requests from the browser. It was introduced to replace the older XMLHttpRequest (XHR) object, providing a cleaner, Promise-based approach to network communication. Fetch is supported in all modern browsers and is the standard way to interact with REST APIs, load data from servers, submit forms programmatically, and download or upload files in web applications.
Unlike XMLHttpRequest, which used callback-based event handling and had a confusing API, the Fetch API uses Promises, making it naturally compatible with async/await syntax. Every call to fetch() returns a Promise that resolves to a Response object, which provides methods to access the response body in various formats like JSON, text, or binary data. Throughout this lesson, you will learn every aspect of the Fetch API, from simple GET requests to complex file uploads, error handling, request cancellation, and real-world API interaction patterns.
The fetch() Syntax
The fetch() function takes two arguments: the URL to request and an optional configuration object. When called with just a URL, it performs a GET request by default. The configuration object lets you specify the HTTP method, headers, body, mode, credentials, and other options.
Example: Basic fetch() Syntax
// Simplest form: GET request with just a URL
fetch('https://api.example.com/data');
// Full form with configuration object
fetch('https://api.example.com/data', {
method: 'GET', // HTTP method
headers: { // Request headers
'Accept': 'application/json',
'Authorization': 'Bearer token123'
},
mode: 'cors', // CORS mode
credentials: 'same-origin', // Cookie handling
cache: 'default', // Cache mode
redirect: 'follow', // Redirect handling
signal: null // AbortController signal
});
fetch() function is available globally in the browser -- you do not need to import or install anything. In Node.js, fetch became available natively starting from version 18. For older Node.js versions, you would need a polyfill like node-fetch.The Response Object
When fetch() resolves, it returns a Response object. This object contains all the information about the server's response, including the HTTP status code, headers, and methods to read the response body. Understanding the Response object is essential for properly handling API responses.
Example: Exploring the Response Object
async function exploreResponse() {
const response = await fetch('https://api.example.com/users/1');
// Response properties
console.log('Status:', response.status); // 200, 404, 500, etc.
console.log('Status Text:', response.statusText); // "OK", "Not Found", etc.
console.log('OK:', response.ok); // true if status is 200-299
console.log('URL:', response.url); // The final URL after redirects
console.log('Redirected:', response.redirected); // true if redirected
console.log('Type:', response.type); // "basic", "cors", "opaque"
// Reading response headers
console.log('Content-Type:', response.headers.get('Content-Type'));
console.log('Cache-Control:', response.headers.get('Cache-Control'));
// Iterating over all headers
for (const [key, value] of response.headers) {
console.log(key + ': ' + value);
}
// Check if a header exists
console.log('Has ETag:', response.headers.has('ETag'));
}
The response.ok property is extremely important. It is true when the HTTP status code is in the range 200-299 (success codes) and false for everything else (client errors like 404, server errors like 500). This is the primary way to check if a request was successful before reading the response body.
Response Body Methods
The Response object provides several methods to read the body of the response. Each method returns a Promise that resolves with the body content in a specific format. You can only read the body once -- after calling one of these methods, the body stream is consumed and cannot be read again unless you clone the response first.
Example: Different Response Body Methods
// 1. response.json() -- Parse as JSON (most common for APIs)
async function getJsonData() {
const response = await fetch('/api/users');
const data = await response.json();
console.log(data); // JavaScript object/array
}
// 2. response.text() -- Read as plain text
async function getTextData() {
const response = await fetch('/api/readme');
const text = await response.text();
console.log(text); // Plain text string
}
// 3. response.blob() -- Read as binary Blob (for files, images)
async function getImageBlob() {
const response = await fetch('/images/photo.jpg');
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
document.getElementById('preview').src = imageUrl;
}
// 4. response.arrayBuffer() -- Read as raw binary data
async function getBinaryData() {
const response = await fetch('/api/download/file.pdf');
const buffer = await response.arrayBuffer();
console.log('Received ' + buffer.byteLength + ' bytes');
}
// 5. response.formData() -- Read as FormData object
async function getFormData() {
const response = await fetch('/api/form-response');
const formData = await response.formData();
console.log(formData.get('username'));
}
// Cloning a response to read the body multiple times
async function readBodyTwice() {
const response = await fetch('/api/users');
const clone = response.clone();
const text = await response.text(); // Read original as text
const json = await clone.json(); // Read clone as JSON
console.log('Text length:', text.length);
console.log('JSON entries:', json.length);
}
response.json() and then response.text() on the same Response object will throw an error because the body stream has already been consumed. If you need to read the body in multiple formats, use response.clone() before reading.GET Requests
GET requests are the most common type of HTTP request. They are used to retrieve data from a server. By default, fetch() makes a GET request, so you only need to provide the URL. GET requests should never modify data on the server -- they are meant to be safe and idempotent.
Example: GET Requests with Query Parameters
// Simple GET request
async function getUsers() {
const response = await fetch('/api/users');
const users = await response.json();
return users;
}
// GET with query parameters using string concatenation
async function searchUsers(query, page = 1) {
const response = await fetch(
'/api/users?search=' + encodeURIComponent(query) + '&page=' + page
);
return response.json();
}
// GET with query parameters using URLSearchParams (recommended)
async function searchUsersClean(query, page = 1, limit = 20) {
const params = new URLSearchParams({
search: query,
page: page,
limit: limit,
sort: 'name'
});
const response = await fetch('/api/users?' + params.toString());
return response.json();
}
// GET with custom headers
async function getUsersAuthenticated(token) {
const response = await fetch('/api/users', {
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch users: ' + response.status);
}
return response.json();
}
URLSearchParams to build query strings instead of manual string concatenation. It automatically handles URL encoding of special characters, preventing broken URLs and potential security issues. You can also pass it directly to the URL constructor for an even cleaner approach.POST Requests
POST requests are used to send data to the server, typically to create new resources. With the Fetch API, you specify method: 'POST' in the configuration object and include the data in the body property. The most common format for sending data is JSON, which requires setting the Content-Type header to application/json.
Example: POST Request with JSON Body
async function createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
name: userData.name,
email: userData.email,
role: userData.role || 'user'
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to create user');
}
const newUser = await response.json();
console.log('User created with ID:', newUser.id);
return newUser;
}
// Usage
const user = await createUser({
name: 'Ahmad Hassan',
email: 'ahmad@example.com',
role: 'admin'
});
Example: POST Request with Form Data
// Sending form data (useful for file uploads)
async function submitForm(formElement) {
const formData = new FormData(formElement);
const response = await fetch('/api/submit', {
method: 'POST',
// Do NOT set Content-Type header -- browser sets it automatically
// with the correct boundary for multipart/form-data
body: formData
});
return response.json();
}
// Building FormData manually
async function uploadProfile(name, email, avatarFile) {
const formData = new FormData();
formData.append('name', name);
formData.append('email', email);
formData.append('avatar', avatarFile, avatarFile.name);
const response = await fetch('/api/profile', {
method: 'POST',
body: formData
});
return response.json();
}
FormData, do not manually set the Content-Type header. The browser automatically sets it to multipart/form-data with the correct boundary string. Setting the header manually will break the request because the boundary will not match.PUT, PATCH, and DELETE Requests
PUT is used to replace an entire resource, PATCH is used to partially update a resource, and DELETE is used to remove a resource. These methods follow the same pattern as POST but with different HTTP methods specified.
Example: PUT, PATCH, and DELETE Requests
// PUT -- Replace entire resource
async function replaceUser(id, userData) {
const response = await fetch('/api/users/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData) // Must include ALL fields
});
return response.json();
}
// PATCH -- Partially update a resource
async function updateUserEmail(id, newEmail) {
const response = await fetch('/api/users/' + id, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: newEmail }) // Only the changed field
});
return response.json();
}
// DELETE -- Remove a resource
async function deleteUser(id) {
const response = await fetch('/api/users/' + id, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + getToken()
}
});
if (!response.ok) {
throw new Error('Failed to delete user: ' + response.status);
}
// Some APIs return 204 No Content for successful deletes
if (response.status === 204) {
return true;
}
return response.json();
}
Sending JSON Data
JSON (JavaScript Object Notation) is the most common data format used in modern web APIs. When sending JSON data with fetch, you need to do two things: set the Content-Type header to application/json and convert your JavaScript object to a JSON string using JSON.stringify().
Example: Sending Various Types of JSON Data
// Sending a simple object
async function createPost(title, content, tags) {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content, tags })
});
return response.json();
}
// Sending nested objects
async function createOrder(items, shippingAddress) {
const orderData = {
items: items.map(item => ({
productId: item.id,
quantity: item.qty,
price: item.price
})),
shipping: {
street: shippingAddress.street,
city: shippingAddress.city,
country: shippingAddress.country,
postalCode: shippingAddress.zip
},
metadata: {
source: 'web',
timestamp: new Date().toISOString()
}
};
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(orderData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
}
// Sending an array as the body
async function batchUpdateStatuses(updates) {
// updates = [{id: 1, status: 'active'}, {id: 2, status: 'inactive'}]
const response = await fetch('/api/users/batch-update', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
return response.json();
}
Sending FormData
FormData is used when you need to send binary files (images, documents, videos) or when you want to replicate a traditional HTML form submission. The FormData API lets you build key-value pairs that are sent as multipart/form-data, which is the encoding type required for file uploads.
Example: File Upload with FormData
// Single file upload
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'Uploaded via web app');
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
// No Content-Type header! Browser sets it automatically.
});
if (!response.ok) {
throw new Error('Upload failed: ' + response.status);
}
return response.json();
}
// Multiple file upload
async function uploadMultipleFiles(files) {
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i], files[i].name);
}
const response = await fetch('/api/upload/batch', {
method: 'POST',
body: formData
});
return response.json();
}
// From an HTML form element
async function submitContactForm() {
const form = document.getElementById('contactForm');
const formData = new FormData(form);
// You can add extra fields programmatically
formData.append('submittedAt', new Date().toISOString());
formData.append('userAgent', navigator.userAgent);
const response = await fetch('/api/contact', {
method: 'POST',
body: formData
});
if (response.ok) {
form.reset();
alert('Message sent successfully!');
}
}
Handling Errors: Network Errors vs HTTP Errors
One of the most important things to understand about the Fetch API is how it handles errors. The fetch() Promise only rejects on network errors -- when the browser cannot reach the server at all (no internet connection, DNS failure, CORS blocked, server down). It does not reject on HTTP error status codes like 404 (Not Found) or 500 (Internal Server Error). You must check response.ok or response.status manually to detect HTTP errors.
Example: Proper Error Handling
async function fetchData(url) {
try {
const response = await fetch(url);
// Check for HTTP errors (fetch does NOT throw for 4xx/5xx)
if (!response.ok) {
// Attempt to read the error message from the response body
let errorMessage = 'HTTP Error: ' + response.status;
try {
const errorBody = await response.json();
errorMessage = errorBody.message || errorMessage;
} catch (parseError) {
// Response body was not JSON, use the default message
}
throw new Error(errorMessage);
}
return await response.json();
} catch (error) {
// This catches BOTH network errors and our thrown HTTP errors
if (error instanceof TypeError) {
// Network error: no connection, DNS failure, CORS blocked
console.error('Network error:', error.message);
throw new Error('Could not connect to the server. Check your internet connection.');
}
// Re-throw HTTP errors and other errors
throw error;
}
}
// A reusable wrapper that standardizes error handling
async function apiFetch(url, options = {}) {
const defaultOptions = {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
...options.headers
},
...options
};
try {
const response = await fetch(url, defaultOptions);
if (!response.ok) {
const error = new Error('API Error');
error.status = response.status;
error.statusText = response.statusText;
try {
error.data = await response.json();
} catch (e) {
error.data = null;
}
throw error;
}
// Handle 204 No Content
if (response.status === 204) {
return null;
}
return response.json();
} catch (error) {
if (!error.status) {
// This is a network error, not an HTTP error
error.status = 0;
error.message = 'Network Error: ' + error.message;
}
throw error;
}
}
fetch() will throw an error for a 404 or 500 response. It will not. The Promise from fetch() only rejects for network failures. A 404 Not Found or 500 Internal Server Error is still a successful HTTP response from the browser's perspective. Always check response.ok before processing the response body.AbortController for Request Cancellation
The AbortController API lets you cancel ongoing fetch requests. This is essential for scenarios like search-as-you-type (where you want to cancel previous searches when the user types new characters), navigating away from a page while data is still loading, or implementing request timeouts.
Example: Cancelling a Fetch Request
// Basic abort example
const controller = new AbortController();
const signal = controller.signal;
fetch('/api/large-dataset', { signal })
.then(response => response.json())
.then(data => console.log('Data received:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
} else {
console.error('Fetch error:', error);
}
});
// Cancel the request after 3 seconds
setTimeout(() => controller.abort(), 3000);
// Implementing a timeout wrapper
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
return response;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timed out after ' + timeoutMs + 'ms');
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
Example: Search-As-You-Type with Abort
class SearchController {
constructor() {
this.currentController = null;
}
async search(query) {
// Cancel any previous search request
if (this.currentController) {
this.currentController.abort();
}
// Create a new controller for this search
this.currentController = new AbortController();
try {
const params = new URLSearchParams({ q: query, limit: 10 });
const response = await fetch('/api/search?' + params, {
signal: this.currentController.signal
});
if (!response.ok) {
throw new Error('Search failed');
}
const results = await response.json();
return results;
} catch (error) {
if (error.name === 'AbortError') {
// This is expected when a new search cancels the old one
console.log('Previous search cancelled');
return null;
}
throw error;
}
}
}
// Usage with a search input
const searcher = new SearchController();
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', async (event) => {
const query = event.target.value.trim();
if (query.length < 2) return;
const results = await searcher.search(query);
if (results) {
displayResults(results);
}
});
Fetch with Async/Await
Combining fetch with async/await creates clean, readable code for complex API interactions. Here is a comprehensive pattern that puts together everything we have learned so far.
Example: Complete API Service Class
class ApiService {
constructor(baseUrl, token = null) {
this.baseUrl = baseUrl;
this.token = token;
}
getHeaders(customHeaders = {}) {
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
...customHeaders
};
if (this.token) {
headers['Authorization'] = 'Bearer ' + this.token;
}
return headers;
}
async request(endpoint, options = {}) {
const url = this.baseUrl + endpoint;
const config = {
headers: this.getHeaders(options.headers),
...options
};
// Remove Content-Type for FormData (browser sets it)
if (options.body instanceof FormData) {
delete config.headers['Content-Type'];
}
const response = await fetch(url, config);
if (!response.ok) {
const error = new Error('API request failed');
error.status = response.status;
try {
error.data = await response.json();
} catch (e) {
error.data = { message: response.statusText };
}
throw error;
}
if (response.status === 204) return null;
return response.json();
}
async get(endpoint, params = {}) {
const query = new URLSearchParams(params).toString();
const url = query ? endpoint + '?' + query : endpoint;
return this.request(url);
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: data instanceof FormData ? data : JSON.stringify(data)
});
}
async put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
async patch(endpoint, data) {
return this.request(endpoint, {
method: 'PATCH',
body: JSON.stringify(data)
});
}
async delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
}
// Usage
const api = new ApiService('https://api.example.com', 'my-auth-token');
async function main() {
// GET all users
const users = await api.get('/users', { page: 1, limit: 20 });
// POST a new user
const newUser = await api.post('/users', {
name: 'Fatima',
email: 'fatima@example.com'
});
// PATCH update
await api.patch('/users/' + newUser.id, { role: 'editor' });
// DELETE
await api.delete('/users/' + newUser.id);
}
Request Headers
HTTP headers let you send additional information with your request. They control authentication, content format, caching, and more. Here are the most commonly used request headers in web development.
Example: Common Request Headers
async function demonstrateHeaders() {
const response = await fetch('/api/data', {
headers: {
// Authentication -- most common patterns
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...',
// or for Basic auth:
// 'Authorization': 'Basic ' + btoa('username:password'),
// Content negotiation
'Accept': 'application/json', // What format you want back
'Content-Type': 'application/json', // What format you are sending
'Accept-Language': 'en-US,en;q=0.9', // Preferred language
// Custom headers (often prefixed with X-)
'X-Request-ID': 'req-' + Date.now(),
'X-Client-Version': '2.1.0',
// Caching
'Cache-Control': 'no-cache',
'If-None-Match': 'W/"abc123"', // ETag for conditional requests
// Security
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
}
});
return response.json();
}
// Using the Headers constructor
async function usingHeadersObject() {
const headers = new Headers();
headers.append('Authorization', 'Bearer token');
headers.append('Accept', 'application/json');
// You can also set, delete, and check headers
headers.set('Accept', 'text/html'); // Replace existing
headers.delete('Accept'); // Remove header
console.log(headers.has('Authorization')); // true
const response = await fetch('/api/data', { headers });
return response.json();
}
X-CSRF-TOKEN header or X-XSRF-TOKEN header. You can read it from the cookie or a meta tag that Laravel includes in the page layout.CORS Basics
CORS (Cross-Origin Resource Sharing) is a security mechanism that controls which websites can make requests to a given server. When your JavaScript code on https://mysite.com tries to fetch data from https://api.other.com, the browser enforces CORS rules. The server must include specific headers in its response to allow cross-origin requests.
Example: CORS and Fetch Mode Options
// Default behavior: cors mode
// Browser sends a preflight OPTIONS request for non-simple requests
async function crossOriginRequest() {
const response = await fetch('https://api.external.com/data', {
mode: 'cors', // Default -- enforces CORS
credentials: 'include' // Send cookies with cross-origin requests
});
return response.json();
}
// Same-origin request (default for same-domain)
async function sameOriginRequest() {
const response = await fetch('/api/data', {
mode: 'same-origin' // Fail if not same origin
});
return response.json();
}
// No-cors mode: limited but useful for certain cases
// Can only use HEAD, GET, or POST with limited headers
// Response is "opaque" -- you cannot read the body
async function noCorsRequest() {
const response = await fetch('https://external.com/resource', {
mode: 'no-cors'
});
// response.type will be "opaque"
// You cannot read response.json() or response.text()
console.log('Response type:', response.type);
}
// Handling CORS errors gracefully
async function safeCrossOrigin(url) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
if (error instanceof TypeError &&
error.message.includes('Failed to fetch')) {
console.error(
'CORS error or network failure. ' +
'The server at ' + url + ' may not allow ' +
'requests from this origin.'
);
}
throw error;
}
}
Access-Control-Allow-Origin) in its response, and the browser decides whether to allow JavaScript to access the response. If you control the server, you can configure it to allow requests from your frontend domain. CORS errors do not occur in server-to-server requests because CORS is purely a browser security feature.Real-World API Interaction Examples
Let us look at complete, production-quality examples that demonstrate common real-world API interactions.
Example 1: Paginated Data Loading
Example: Loading Paginated Data
class PaginatedList {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.items = [];
this.currentPage = 0;
this.totalPages = 1;
this.loading = false;
this.hasMore = true;
}
async loadNextPage() {
if (this.loading || !this.hasMore) return;
this.loading = true;
const nextPage = this.currentPage + 1;
try {
const params = new URLSearchParams({
page: nextPage,
per_page: 20
});
const response = await fetch(this.baseUrl + '?' + params);
if (!response.ok) throw new Error('Failed to load page ' + nextPage);
const data = await response.json();
this.items = this.items.concat(data.items);
this.currentPage = data.currentPage;
this.totalPages = data.totalPages;
this.hasMore = this.currentPage < this.totalPages;
return data.items;
} catch (error) {
console.error('Pagination error:', error.message);
throw error;
} finally {
this.loading = false;
}
}
async loadAll() {
while (this.hasMore) {
await this.loadNextPage();
}
return this.items;
}
}
// Usage with infinite scroll
const productList = new PaginatedList('/api/products');
window.addEventListener('scroll', async () => {
const nearBottom = window.innerHeight + window.scrollY
>= document.body.offsetHeight - 500;
if (nearBottom && productList.hasMore && !productList.loading) {
const newItems = await productList.loadNextPage();
if (newItems) {
renderProducts(newItems);
}
}
});
Example 2: File Upload with Progress
Example: File Upload with Progress Tracking
// Note: fetch() does not natively support upload progress.
// For upload progress, use XMLHttpRequest or a library.
// However, fetch works great for downloads and basic uploads.
async function uploadWithFetch(file, onProgress) {
const formData = new FormData();
formData.append('file', file);
// For simple upload (no progress tracking)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Upload failed with status ' + response.status);
}
return response.json();
}
// Download with progress using response.body stream
async function downloadWithProgress(url, onProgress) {
const response = await fetch(url);
if (!response.ok) throw new Error('Download failed');
const contentLength = parseInt(response.headers.get('Content-Length') || '0');
const reader = response.body.getReader();
const chunks = [];
let receivedLength = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
if (contentLength > 0 && onProgress) {
const percent = Math.round((receivedLength / contentLength) * 100);
onProgress(percent, receivedLength, contentLength);
}
}
// Combine chunks into a single array
const allChunks = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
allChunks.set(chunk, position);
position += chunk.length;
}
return allChunks;
}
// Usage
downloadWithProgress('/api/files/large-report.pdf', (percent, received, total) => {
console.log('Download progress: ' + percent + '%');
document.getElementById('progress').style.width = percent + '%';
}).then(data => {
const blob = new Blob([data], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
window.open(url);
});
Example 3: Complete CRUD Application
Example: Task Management CRUD
class TaskAPI {
constructor() {
this.baseUrl = '/api/tasks';
this.token = localStorage.getItem('authToken');
}
getHeaders() {
return {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ' + this.token
};
}
// CREATE
async createTask(title, description, dueDate) {
const response = await fetch(this.baseUrl, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify({
title: title,
description: description,
due_date: dueDate,
status: 'pending'
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create task');
}
return response.json();
}
// READ (list)
async getTasks(filters = {}) {
const params = new URLSearchParams();
if (filters.status) params.append('status', filters.status);
if (filters.sort) params.append('sort', filters.sort);
if (filters.page) params.append('page', filters.page);
const url = this.baseUrl + (params.toString() ? '?' + params : '');
const response = await fetch(url, {
headers: this.getHeaders()
});
if (!response.ok) throw new Error('Failed to load tasks');
return response.json();
}
// READ (single)
async getTask(id) {
const response = await fetch(this.baseUrl + '/' + id, {
headers: this.getHeaders()
});
if (response.status === 404) {
throw new Error('Task not found');
}
if (!response.ok) throw new Error('Failed to load task');
return response.json();
}
// UPDATE
async updateTask(id, updates) {
const response = await fetch(this.baseUrl + '/' + id, {
method: 'PATCH',
headers: this.getHeaders(),
body: JSON.stringify(updates)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to update task');
}
return response.json();
}
// DELETE
async deleteTask(id) {
const response = await fetch(this.baseUrl + '/' + id, {
method: 'DELETE',
headers: this.getHeaders()
});
if (!response.ok) throw new Error('Failed to delete task');
return response.status === 204 ? true : response.json();
}
// BATCH operations
async markMultipleComplete(taskIds) {
const response = await fetch(this.baseUrl + '/batch-update', {
method: 'PATCH',
headers: this.getHeaders(),
body: JSON.stringify({
ids: taskIds,
updates: { status: 'completed' }
})
});
if (!response.ok) throw new Error('Batch update failed');
return response.json();
}
}
// Using the TaskAPI
const taskApi = new TaskAPI();
async function initTaskManager() {
try {
// Load all pending tasks
const tasks = await taskApi.getTasks({
status: 'pending',
sort: 'due_date'
});
renderTaskList(tasks);
// Create a new task
const newTask = await taskApi.createTask(
'Review pull requests',
'Review the 3 pending PRs on the main repository',
'2025-06-15'
);
console.log('Created task:', newTask.id);
// Update the task
await taskApi.updateTask(newTask.id, {
status: 'in_progress',
priority: 'high'
});
// Delete a task
await taskApi.deleteTask(newTask.id);
} catch (error) {
console.error('Task manager error:', error.message);
}
}
Example 4: Handling Authentication Expiration
Example: Auto-Refresh Token on 401
class AuthenticatedFetch {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.token = localStorage.getItem('accessToken');
this.refreshToken = localStorage.getItem('refreshToken');
this.refreshing = null; // Holds the refresh promise to avoid duplicates
}
async fetch(endpoint, options = {}) {
const url = this.baseUrl + endpoint;
const config = {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.token,
...options.headers
}
};
let response = await fetch(url, config);
// If unauthorized, try refreshing the token
if (response.status === 401 && this.refreshToken) {
await this.doRefresh();
// Retry the original request with the new token
config.headers['Authorization'] = 'Bearer ' + this.token;
response = await fetch(url, config);
}
if (!response.ok) {
throw new Error('Request failed: ' + response.status);
}
if (response.status === 204) return null;
return response.json();
}
async doRefresh() {
// If already refreshing, wait for the existing refresh to complete
if (this.refreshing) {
return this.refreshing;
}
this.refreshing = (async () => {
try {
const response = await fetch(this.baseUrl + '/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: this.refreshToken })
});
if (!response.ok) {
throw new Error('Refresh failed');
}
const data = await response.json();
this.token = data.access_token;
this.refreshToken = data.refresh_token;
localStorage.setItem('accessToken', this.token);
localStorage.setItem('refreshToken', this.refreshToken);
} catch (error) {
// Refresh failed -- user needs to log in again
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
throw error;
} finally {
this.refreshing = null;
}
})();
return this.refreshing;
}
}
// Usage
const api = new AuthenticatedFetch('https://api.example.com');
async function loadUserData() {
const profile = await api.fetch('/profile');
const orders = await api.fetch('/orders?limit=5');
return { profile, orders };
}
Fetch Configuration Best Practices
Here is a summary of recommended configuration patterns for different types of requests.
Example: Configuration Reference
// Reading data (GET)
fetch(url, {
method: 'GET',
headers: { 'Accept': 'application/json' },
credentials: 'same-origin'
});
// Sending JSON (POST/PUT/PATCH)
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(data)
});
// Uploading files
fetch(url, {
method: 'POST',
// No Content-Type header for FormData!
body: formData
});
// Authenticated request
fetch(url, {
headers: { 'Authorization': 'Bearer ' + token }
});
// Request with timeout
const controller = new AbortController();
setTimeout(() => controller.abort(), 10000);
fetch(url, { signal: controller.signal });
// Cross-origin with cookies
fetch(url, {
mode: 'cors',
credentials: 'include'
});
Practice Exercise
Build a complete blog client application using the Fetch API and async/await. Use a free API like JSONPlaceholder (https://jsonplaceholder.typicode.com) as your backend. Create a BlogService class with methods for all CRUD operations: getPosts(page, limit) to fetch paginated posts, getPost(id) to fetch a single post, createPost(title, body) to create a new post, updatePost(id, updates) to update a post with PATCH, and deletePost(id) to delete a post. Add a getPostWithComments(id) method that uses Promise.all() to fetch the post and its comments in parallel. Implement proper error handling that distinguishes between network errors and HTTP errors. Add an AbortController-based search method that cancels previous search requests when a new one is made. Add a fetchWithRetry() helper that retries failed requests up to 3 times with exponential backoff. Write a downloadPostsAsJson() method that fetches all posts and creates a downloadable Blob file. Test every method and verify that network error messages are different from HTTP error messages.