Skip to main content

Chapter 10: JavaScript Fetch API - Complete Guide to Network Requests

Fetch API is the modern JavaScript interface for making HTTP requests. It provides a powerful, flexible, and promise-based way to interact with web servers, replacing the older XMLHttpRequest with a cleaner, more intuitive API.

Why Fetch API is Essential in JavaScript

Fetch API in JavaScript is crucial because it:

  • Enables Modern Web Development: Provides a standardized way to make HTTP requests
  • Supports Async/Await: Works seamlessly with modern asynchronous JavaScript patterns
  • Handles Various Data Types: Supports JSON, FormData, Blob, and other data formats
  • Provides Better Error Handling: More intuitive error management than XMLHttpRequest
  • Supports Modern Features: CORS, streaming, and advanced request options
  • Enables Real-time Applications: Essential for building dynamic, data-driven web applications

Learning Objectives

Through this chapter, you will master:

  • Basic Fetch API usage and syntax
  • HTTP methods (GET, POST, PUT, DELETE)
  • Request and response handling
  • Error handling and status codes
  • Authentication and headers
  • File uploads and FormData
  • Advanced features and best practices
  • Performance optimization techniques

Basic Fetch API Usage

Simple GET Request

// Basic GET request
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

// Using async/await
async function fetchUsers() {
try {
const response = await fetch('https://api.example.com/users');
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
}

// Check response status
async function fetchUsersWithStatusCheck() {
try {
const response = await fetch('https://api.example.com/users');

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

const data = await response.json();
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}

Response Handling

// Different response types
async function handleDifferentResponseTypes() {
try {
// JSON response
const jsonResponse = await fetch('/api/data.json');
const jsonData = await jsonResponse.json();
console.log('JSON data:', jsonData);

// Text response
const textResponse = await fetch('/api/data.txt');
const textData = await textResponse.text();
console.log('Text data:', textData);

// Blob response (for files)
const blobResponse = await fetch('/api/image.jpg');
const blobData = await blobResponse.blob();
console.log('Blob data:', blobData);

// ArrayBuffer response
const bufferResponse = await fetch('/api/binary.data');
const bufferData = await bufferResponse.arrayBuffer();
console.log('Buffer data:', bufferData);

} catch (error) {
console.error('Error:', error);
}
}

// Response headers
async function getResponseHeaders() {
try {
const response = await fetch('/api/data');

// Get specific header
const contentType = response.headers.get('content-type');
console.log('Content-Type:', contentType);

// Get all headers
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
console.log('All headers:', headers);

// Check if header exists
if (response.headers.has('authorization')) {
console.log('Authorization header present');
}

} catch (error) {
console.error('Error:', error);
}
}

HTTP Methods

POST Request

// Basic POST request
async function createUser(userData) {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});

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

const newUser = await response.json();
return newUser;
} catch (error) {
console.error('Error creating user:', error);
throw error;
}
}

// Usage
const userData = {
name: 'John Doe',
email: '[email protected]',
age: 30
};

createUser(userData)
.then(user => console.log('Created user:', user))
.catch(error => console.error('Failed to create user:', error));

PUT and PATCH Requests

// PUT request (full update)
async function updateUser(userId, userData) {
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});

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

const updatedUser = await response.json();
return updatedUser;
} catch (error) {
console.error('Error updating user:', error);
throw error;
}
}

// PATCH request (partial update)
async function patchUser(userId, partialData) {
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(partialData)
});

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

const updatedUser = await response.json();
return updatedUser;
} catch (error) {
console.error('Error patching user:', error);
throw error;
}
}

// Usage examples
updateUser(1, { name: 'John Smith', email: '[email protected]', age: 31 });
patchUser(1, { age: 32 }); // Only update age

DELETE Request

// DELETE request
async function deleteUser(userId) {
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'DELETE'
});

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

// DELETE requests often return 204 No Content
if (response.status === 204) {
return { success: true, message: 'User deleted successfully' };
}

const result = await response.json();
return result;
} catch (error) {
console.error('Error deleting user:', error);
throw error;
}
}

// Usage
deleteUser(1)
.then(result => console.log('Delete result:', result))
.catch(error => console.error('Failed to delete user:', error));

Error Handling

Comprehensive Error Handling

// Enhanced error handling
class ApiError extends Error {
constructor(message, status, statusText) {
super(message);
this.name = 'ApiError';
this.status = status;
this.statusText = statusText;
}
}

async function fetchWithErrorHandling(url, options = {}) {
try {
const response = await fetch(url, options);

// Handle different status codes
if (response.status >= 200 && response.status < 300) {
return await response.json();
} else if (response.status === 401) {
throw new ApiError('Unauthorized - Please log in', 401, 'Unauthorized');
} else if (response.status === 403) {
throw new ApiError('Forbidden - Access denied', 403, 'Forbidden');
} else if (response.status === 404) {
throw new ApiError('Not found - Resource does not exist', 404, 'Not Found');
} else if (response.status === 422) {
const errorData = await response.json();
throw new ApiError(`Validation error: ${errorData.message}`, 422, 'Unprocessable Entity');
} else if (response.status >= 500) {
throw new ApiError('Server error - Please try again later', response.status, response.statusText);
} else {
throw new ApiError(`HTTP error: ${response.status}`, response.status, response.statusText);
}
} catch (error) {
if (error instanceof ApiError) {
throw error;
} else if (error.name === 'TypeError' && error.message.includes('fetch')) {
throw new ApiError('Network error - Please check your connection', 0, 'Network Error');
} else {
throw new ApiError(`Unexpected error: ${error.message}`, 0, 'Unknown Error');
}
}
}

// Usage with error handling
async function fetchUserData(userId) {
try {
const userData = await fetchWithErrorHandling(`/api/users/${userId}`);
return userData;
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error (${error.status}): ${error.message}`);

// Handle specific errors
switch (error.status) {
case 401:
// Redirect to login
window.location.href = '/login';
break;
case 404:
// Show not found message
showMessage('User not found', 'error');
break;
case 422:
// Show validation errors
showMessage(error.message, 'error');
break;
default:
showMessage('An error occurred. Please try again.', 'error');
}
} else {
console.error('Unexpected error:', error);
showMessage('An unexpected error occurred.', 'error');
}
throw error;
}
}

Timeout Handling

// Fetch with timeout
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

try {
const response = await fetch(url, {
...options,
signal: controller.signal
});

clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);

if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeout}ms`);
}

throw error;
}
}

// Usage
fetchWithTimeout('/api/slow-endpoint', {}, 3000)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.message.includes('timeout')) {
console.log('Request timed out');
} else {
console.error('Other error:', error);
}
});

Authentication and Headers

Basic Authentication

// Basic authentication
async function fetchWithAuth(url, options = {}) {
const token = localStorage.getItem('authToken');

const headers = {
'Content-Type': 'application/json',
...options.headers
};

if (token) {
headers['Authorization'] = `Bearer ${token}`;
}

return fetch(url, {
...options,
headers
});
}

// Usage
async function getProtectedData() {
try {
const response = await fetchWithAuth('/api/protected-data');

if (response.status === 401) {
// Token expired, redirect to login
localStorage.removeItem('authToken');
window.location.href = '/login';
return;
}

const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching protected data:', error);
throw error;
}
}

Custom Headers

// Custom headers
async function fetchWithCustomHeaders(url, options = {}) {
const defaultHeaders = {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-Client-Version': '1.0.0',
'X-Client-Platform': navigator.platform
};

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

return fetch(url, {
...options,
headers
});
}

// API key authentication
async function fetchWithApiKey(url, options = {}) {
const apiKey = process.env.API_KEY || 'your-api-key';

return fetch(url, {
...options,
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json',
...options.headers
}
});
}

File Uploads

Single File Upload

// Single file upload
async function uploadFile(file, uploadUrl) {
const formData = new FormData();
formData.append('file', file);

try {
const response = await fetch(uploadUrl, {
method: 'POST',
body: formData
// Don't set Content-Type header - let browser set it with boundary
});

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

const result = await response.json();
return result;
} catch (error) {
console.error('File upload error:', error);
throw error;
}
}

// File upload with progress
async function uploadFileWithProgress(file, uploadUrl, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);

xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
onProgress(percentComplete);
}
});

xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});

xhr.addEventListener('error', () => {
reject(new Error('Upload failed'));
});

xhr.open('POST', uploadUrl);
xhr.send(formData);
});
}

// Usage
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file) {
try {
await uploadFileWithProgress(file, '/api/upload', (progress) => {
console.log(`Upload progress: ${progress.toFixed(2)}%`);
});
console.log('File uploaded successfully');
} catch (error) {
console.error('Upload failed:', error);
}
}
});

Multiple File Upload

// Multiple file upload
async function uploadMultipleFiles(files, uploadUrl) {
const formData = new FormData();

// Add multiple files
files.forEach((file, index) => {
formData.append(`files`, file);
});

try {
const response = await fetch(uploadUrl, {
method: 'POST',
body: formData
});

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

const result = await response.json();
return result;
} catch (error) {
console.error('Multiple file upload error:', error);
throw error;
}
}

// Drag and drop file upload
function setupDragAndDrop(dropZone, uploadUrl) {
dropZone.addEventListener('dragover', (event) => {
event.preventDefault();
dropZone.classList.add('drag-over');
});

dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});

dropZone.addEventListener('drop', async (event) => {
event.preventDefault();
dropZone.classList.remove('drag-over');

const files = Array.from(event.dataTransfer.files);

if (files.length > 0) {
try {
const result = await uploadMultipleFiles(files, uploadUrl);
console.log('Files uploaded:', result);
} catch (error) {
console.error('Upload failed:', error);
}
}
});
}

Advanced Features

Request Interceptors

// Request interceptor
class FetchInterceptor {
constructor() {
this.requestInterceptors = [];
this.responseInterceptors = [];
}

addRequestInterceptor(interceptor) {
this.requestInterceptors.push(interceptor);
}

addResponseInterceptor(interceptor) {
this.responseInterceptors.push(interceptor);
}

async fetch(url, options = {}) {
// Apply request interceptors
let modifiedOptions = { ...options };
for (const interceptor of this.requestInterceptors) {
modifiedOptions = await interceptor(url, modifiedOptions);
}

// Make the request
let response = await fetch(url, modifiedOptions);

// Apply response interceptors
for (const interceptor of this.responseInterceptors) {
response = await interceptor(response);
}

return response;
}
}

// Usage
const apiClient = new FetchInterceptor();

// Add request interceptor for authentication
apiClient.addRequestInterceptor(async (url, options) => {
const token = localStorage.getItem('authToken');
if (token) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${token}`
};
}
return options;
});

// Add response interceptor for error handling
apiClient.addResponseInterceptor(async (response) => {
if (response.status === 401) {
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return response;
});

// Use the interceptor
apiClient.fetch('/api/protected-data')
.then(response => response.json())
.then(data => console.log(data));

Caching and Cache Control

// Cache control
async function fetchWithCache(url, options = {}) {
const cacheKey = `cache_${url}`;
const cachedData = localStorage.getItem(cacheKey);
const cacheTimestamp = localStorage.getItem(`${cacheKey}_timestamp`);

// Check if cache is still valid (5 minutes)
const cacheExpiry = 5 * 60 * 1000;
const now = Date.now();

if (cachedData && cacheTimestamp && (now - parseInt(cacheTimestamp)) < cacheExpiry) {
console.log('Returning cached data');
return JSON.parse(cachedData);
}

try {
const response = await fetch(url, {
...options,
headers: {
'Cache-Control': 'no-cache',
...options.headers
}
});

if (response.ok) {
const data = await response.json();

// Cache the data
localStorage.setItem(cacheKey, JSON.stringify(data));
localStorage.setItem(`${cacheKey}_timestamp`, now.toString());

return data;
}

throw new Error(`HTTP error! status: ${response.status}`);
} catch (error) {
// Return cached data if available, even if expired
if (cachedData) {
console.log('Network error, returning stale cache');
return JSON.parse(cachedData);
}

throw error;
}
}

Best Practices

1. Create a Reusable API Client

// API client class
class ApiClient {
constructor(baseURL, defaultOptions = {}) {
this.baseURL = baseURL;
this.defaultOptions = {
headers: {
'Content-Type': 'application/json',
...defaultOptions.headers
},
...defaultOptions
};
}

async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...this.defaultOptions,
...options,
headers: {
...this.defaultOptions.headers,
...options.headers
}
};

try {
const response = await fetch(url, config);

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

return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}

get(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'GET' });
}

post(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
}

put(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'PUT',
body: JSON.stringify(data)
});
}

delete(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'DELETE' });
}
}

// Usage
const api = new ApiClient('https://api.example.com');

// Get users
api.get('/users')
.then(users => console.log(users))
.catch(error => console.error(error));

// Create user
api.post('/users', { name: 'John', email: '[email protected]' })
.then(user => console.log('Created:', user))
.catch(error => console.error(error));

2. Handle Loading States

// Loading state management
class LoadingManager {
constructor() {
this.loadingStates = new Map();
}

setLoading(key, isLoading) {
this.loadingStates.set(key, isLoading);
this.notifyListeners();
}

isLoading(key) {
return this.loadingStates.get(key) || false;
}

addListener(callback) {
this.listeners = this.listeners || [];
this.listeners.push(callback);
}

notifyListeners() {
if (this.listeners) {
this.listeners.forEach(callback => callback(this.loadingStates));
}
}
}

const loadingManager = new LoadingManager();

// Fetch with loading state
async function fetchWithLoading(key, url, options = {}) {
loadingManager.setLoading(key, true);

try {
const response = await fetch(url, options);
const data = await response.json();
return data;
} finally {
loadingManager.setLoading(key, false);
}
}

// Usage
fetchWithLoading('users', '/api/users')
.then(users => console.log(users))
.catch(error => console.error(error));

3. Retry Logic

// Retry logic for failed requests
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);

if (response.ok) {
return response;
}

// Don't retry on client errors (4xx)
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}

throw new Error(`Server error: ${response.status}`);
} catch (error) {
lastError = error;

if (attempt === maxRetries) {
throw lastError;
}

// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

Summary

Fetch API is essential for modern web development:

  • Basic Usage: Simple GET, POST, PUT, DELETE requests
  • Response Handling: JSON, text, blob, and other data types
  • Error Handling: Comprehensive error management and status codes
  • Authentication: Bearer tokens, API keys, and custom headers
  • File Uploads: Single and multiple file uploads with progress
  • Advanced Features: Interceptors, caching, and retry logic
  • Best Practices: Reusable API clients and loading state management

Mastering Fetch API enables you to build robust, interactive web applications that communicate effectively with backend services and provide excellent user experiences.


This tutorial is part of the JavaScript Mastery series by syscook.dev