JavaScript Fetch API: Modern HTTP Requests
Master the Fetch API for making HTTP requests in JavaScript. Learn GET, POST, error handling, headers, and advanced patterns.
JavaScript Fetch API: Modern HTTP Requests
The Fetch API provides a modern, powerful, and flexible way to make HTTP requests in JavaScript. It's the successor to XMLHttpRequest and uses Promises for better async handling.
Introduction to Fetch
The Fetch API is a modern interface for making HTTP requests that returns Promises and provides a more powerful and flexible feature set than XMLHttpRequest.
// Basic fetch request
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error('Error:', error));
// Using async/await
async function getData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
Making GET Requests
Basic GET Request
// Simple GET request
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((users) => {
console.log('Users:', users);
})
.catch((error) => {
console.error('Fetch error:', error);
});
// With async/await
async function getUsers() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const users = await response.json();
return users;
}
// With query parameters
const params = new URLSearchParams({
page: 1,
limit: 10,
sort: 'name',
});
fetch(`https://api.example.com/users?${params}`)
.then((response) => response.json())
.then((data) => console.log(data));
GET Request with Headers
// Custom headers
fetch('https://api.example.com/protected-data', {
method: 'GET', // Optional, GET is default
headers: {
Authorization: 'Bearer YOUR_TOKEN_HERE',
Accept: 'application/json',
'X-Custom-Header': 'CustomValue',
},
})
.then((response) => response.json())
.then((data) => console.log(data));
// Dynamic headers
async function fetchWithAuth(url, token) {
const headers = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(url, { headers });
return response.json();
}
Making POST Requests
Sending JSON Data
// POST with JSON data
const userData = {
name: 'John Doe',
email: 'john@example.com',
age: 30,
};
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
})
.then((response) => response.json())
.then((data) => {
console.log('Success:', data);
})
.catch((error) => {
console.error('Error:', error);
});
// With async/await
async function createUser(userData) {
try {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error('Error creating user:', error);
throw error;
}
}
Sending Form Data
// FormData for file uploads
const formData = new FormData();
formData.append('username', 'john_doe');
formData.append('email', 'john@example.com');
formData.append('avatar', fileInput.files[0]);
fetch('https://api.example.com/upload', {
method: 'POST',
body: formData, // No Content-Type header needed
})
.then((response) => response.json())
.then((result) => console.log('Upload successful:', result));
// URL-encoded form data
const urlEncodedData = new URLSearchParams();
urlEncodedData.append('username', 'john_doe');
urlEncodedData.append('password', 'secure_password');
fetch('https://api.example.com/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: urlEncodedData,
})
.then((response) => response.json())
.then((data) => console.log(data));
Other HTTP Methods
PUT Request
// Update resource
const updatedData = {
id: 1,
name: 'Jane Doe',
email: 'jane@example.com',
};
fetch('https://api.example.com/users/1', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedData),
})
.then((response) => response.json())
.then((data) => console.log('Updated:', data));
// PATCH for partial updates
fetch('https://api.example.com/users/1', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'Jane Smith' }),
})
.then((response) => response.json())
.then((data) => console.log('Patched:', data));
DELETE Request
// Delete resource
fetch('https://api.example.com/users/1', {
method: 'DELETE',
headers: {
Authorization: 'Bearer TOKEN',
},
})
.then((response) => {
if (response.ok) {
console.log('Deleted successfully');
} else {
throw new Error('Delete failed');
}
})
.catch((error) => console.error('Error:', error));
// DELETE with confirmation
async function deleteUser(userId) {
if (!confirm('Are you sure you want to delete this user?')) {
return;
}
try {
const response = await fetch(`https://api.example.com/users/${userId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${getAuthToken()}`,
},
});
if (!response.ok) {
throw new Error('Delete failed');
}
return true;
} catch (error) {
console.error('Delete error:', error);
return false;
}
}
Handling Responses
Response Object
fetch('https://api.example.com/data')
.then((response) => {
// Response properties
console.log('Status:', response.status);
console.log('Status Text:', response.statusText);
console.log('OK:', response.ok); // true if status 200-299
console.log('Headers:', response.headers);
console.log('URL:', response.url);
console.log('Type:', response.type);
// Get specific header
console.log('Content-Type:', response.headers.get('Content-Type'));
// Check status
if (response.ok) {
return response.json();
} else if (response.status === 404) {
throw new Error('Not found');
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
})
.then((data) => console.log(data))
.catch((error) => console.error(error));
Different Response Types
// JSON response
fetch('/api/data')
.then((response) => response.json())
.then((data) => console.log(data));
// Text response
fetch('/api/text')
.then((response) => response.text())
.then((text) => console.log(text));
// Blob response (for files)
fetch('/api/image')
.then((response) => response.blob())
.then((blob) => {
const url = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);
});
// ArrayBuffer response
fetch('/api/binary')
.then((response) => response.arrayBuffer())
.then((buffer) => {
const view = new DataView(buffer);
console.log(view);
});
// FormData response
fetch('/api/form-data')
.then((response) => response.formData())
.then((formData) => {
for (let [key, value] of formData) {
console.log(`${key}: ${value}`);
}
});
Error Handling
Comprehensive Error Handling
class FetchError extends Error {
constructor(response) {
super(`HTTP Error ${response.status}: ${response.statusText}`);
this.name = 'FetchError';
this.response = response;
}
}
async function fetchWithErrorHandling(url, options = {}) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new FetchError(response);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else if (contentType && contentType.includes('text')) {
return await response.text();
} else {
return response;
}
} catch (error) {
if (error instanceof FetchError) {
// Handle HTTP errors
console.error('HTTP Error:', error.message);
// Try to get error details from response body
try {
const errorData = await error.response.json();
console.error('Error details:', errorData);
} catch (e) {
// Response body is not JSON
}
} else if (error instanceof TypeError) {
// Network error or CORS issue
console.error('Network error:', error.message);
} else {
// Other errors
console.error('Unexpected error:', error);
}
throw error;
}
}
Retry Logic
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Attempt ${i + 1} failed:`, error);
if (i === retries - 1) {
throw error;
}
// Exponential backoff
await new Promise((resolve) =>
setTimeout(resolve, delay * Math.pow(2, i))
);
}
}
}
// Usage
fetchWithRetry('https://api.example.com/data', {}, 3, 1000)
.then((data) => console.log('Success:', data))
.catch((error) => console.error('Failed after retries:', error));
Advanced Fetch Patterns
Request Interceptor
class FetchInterceptor {
constructor() {
this.interceptors = {
request: [],
response: [],
};
}
addRequestInterceptor(interceptor) {
this.interceptors.request.push(interceptor);
}
addResponseInterceptor(interceptor) {
this.interceptors.response.push(interceptor);
}
async fetch(url, options = {}) {
// Apply request interceptors
let modifiedOptions = { ...options };
for (const interceptor of this.interceptors.request) {
modifiedOptions = await interceptor(url, modifiedOptions);
}
// Make the request
let response = await fetch(url, modifiedOptions);
// Apply response interceptors
for (const interceptor of this.interceptors.response) {
response = await interceptor(response);
}
return response;
}
}
// Usage
const api = new FetchInterceptor();
// Add auth token to all requests
api.addRequestInterceptor(async (url, options) => {
const token = localStorage.getItem('auth_token');
if (token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token}`,
};
}
return options;
});
// Log all responses
api.addResponseInterceptor(async (response) => {
console.log(`Response from ${response.url}: ${response.status}`);
return response;
});
// Handle 401 responses
api.addResponseInterceptor(async (response) => {
if (response.status === 401) {
// Redirect to login
window.location.href = '/login';
}
return response;
});
Request Cancellation
// Using AbortController
class CancellableFetch {
constructor() {
this.controllers = new Map();
}
async fetch(id, url, options = {}) {
// Cancel any existing request with same ID
this.cancel(id);
// Create new controller
const controller = new AbortController();
this.controllers.set(id, controller);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
this.controllers.delete(id);
return response;
} catch (error) {
if (error.name === 'AbortError') {
console.log(`Request ${id} was cancelled`);
} else {
throw error;
}
}
}
cancel(id) {
const controller = this.controllers.get(id);
if (controller) {
controller.abort();
this.controllers.delete(id);
}
}
cancelAll() {
for (const controller of this.controllers.values()) {
controller.abort();
}
this.controllers.clear();
}
}
// Usage
const api = new CancellableFetch();
// Start a request
api
.fetch('user-search', '/api/users?q=john')
.then((response) => response.json())
.then((users) => console.log(users));
// Cancel it if user types again
api.cancel('user-search');
Parallel Requests
// Fetch multiple resources in parallel
async function fetchMultiple(urls) {
const promises = urls.map((url) => fetch(url).then((r) => r.json()));
try {
const results = await Promise.all(promises);
return results;
} catch (error) {
console.error('One or more requests failed:', error);
throw error;
}
}
// With individual error handling
async function fetchMultipleWithErrorHandling(urls) {
const promises = urls.map(async (url) => {
try {
const response = await fetch(url);
return {
url,
status: 'success',
data: await response.json(),
};
} catch (error) {
return {
url,
status: 'error',
error: error.message,
};
}
});
return Promise.all(promises);
}
// Fetch with concurrency limit
async function fetchWithConcurrencyLimit(urls, limit = 3) {
const results = [];
const executing = [];
for (const url of urls) {
const promise = fetch(url).then((r) => r.json());
results.push(promise);
if (urls.length >= limit) {
executing.push(promise);
if (executing.length >= limit) {
await Promise.race(executing);
executing.splice(
executing.findIndex((p) => p === promise),
1
);
}
}
}
return Promise.all(results);
}
Request/Response Caching
class FetchCache {
constructor(ttl = 5 * 60 * 1000) {
// 5 minutes default
this.cache = new Map();
this.ttl = ttl;
}
getCacheKey(url, options = {}) {
return `${url}:${JSON.stringify(options)}`;
}
async fetch(url, options = {}) {
const key = this.getCacheKey(url, options);
// Check cache
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
console.log('Returning cached response for:', url);
return cached.response.clone();
}
// Make fresh request
const response = await fetch(url, options);
// Cache successful responses
if (response.ok) {
this.cache.set(key, {
response: response.clone(),
timestamp: Date.now(),
});
}
return response;
}
clear() {
this.cache.clear();
}
remove(url, options = {}) {
const key = this.getCacheKey(url, options);
this.cache.delete(key);
}
}
// Usage with cache
const cachedFetch = new FetchCache();
// First call - makes network request
await cachedFetch.fetch('/api/users');
// Second call within TTL - returns cached
await cachedFetch.fetch('/api/users');
Real-World Examples
API Client Class
class APIClient {
constructor(baseURL, options = {}) {
this.baseURL = baseURL;
this.headers = options.headers || {};
this.timeout = options.timeout || 30000;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...options,
headers: {
'Content-Type': 'application/json',
...this.headers,
...options.headers,
},
};
// Add timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url, {
...config,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
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' });
}
setAuthToken(token) {
if (token) {
this.headers['Authorization'] = `Bearer ${token}`;
} else {
delete this.headers['Authorization'];
}
}
}
// Usage
const api = new APIClient('https://api.example.com', {
timeout: 10000,
});
api.setAuthToken('your-token-here');
// GET request
const users = await api.get('/users');
// POST request
const newUser = await api.post('/users', {
name: 'John Doe',
email: 'john@example.com',
});
File Upload with Progress
function uploadFileWithProgress(file, url, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
// Progress tracking
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.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', url);
xhr.send(formData);
});
}
// Fetch doesn't support upload progress, so we can combine approaches
class UploadManager {
async uploadWithFetch(file, url) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(url, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Upload failed');
}
return response.json();
}
async uploadMultiple(files, url, onProgress) {
const total = files.length;
let completed = 0;
const promises = files.map(async (file, index) => {
const result = await this.uploadWithFetch(file, url);
completed++;
onProgress((completed / total) * 100, index);
return result;
});
return Promise.all(promises);
}
}
Best Practices
-
Always check response.ok
const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
-
Set appropriate headers
fetch(url, { headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, });
-
Handle network errors
try { const response = await fetch(url); // Handle response } catch (error) { if (error instanceof TypeError) { console.error('Network error:', error); } }
-
Use AbortController for cancellation
const controller = new AbortController(); fetch(url, { signal: controller.signal }).catch((err) => { if (err.name === 'AbortError') { console.log('Fetch aborted'); } }); // Cancel the request controller.abort();
Conclusion
The Fetch API is a powerful tool for making HTTP requests in modern JavaScript:
- Promise-based for better async handling
- Flexible request configuration
- Multiple response types support
- Better error handling than XMLHttpRequest
- Streaming capabilities for large responses
- AbortController for cancellation
Key takeaways:
- Always handle both network and HTTP errors
- Use appropriate headers for content types
- Implement retry logic for resilience
- Consider caching for performance
- Use AbortController for cancellable requests
- Build reusable API clients for consistency
Master the Fetch API to build robust, modern web applications with efficient data fetching!