# Fetch API Complete Guide: Modern HTTP Requests Explained
Master Fetch request configuration, response handling, interceptor patterns and error handling
The Fetch API is the standard HTTP request interface for modern browsers. This article explores various Fetch usage patterns and best practices.
Basic Usage
Simple Requests
// GET request
const response = await fetch('https://api.example.com/users');
const data = await response.json();
// Check response status
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// POST request
const createUser = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'John',
email: 'john@example.com'
})
});
Request Configuration
const response = await fetch(url, {
// HTTP method
method: 'POST', // GET, POST, PUT, DELETE, PATCH
// Request headers
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
'X-Custom-Header': 'value'
},
// Request body
body: JSON.stringify(data),
// Credentials mode
credentials: 'include', // include, same-origin, omit
// Cache mode
cache: 'no-cache', // default, no-store, reload, no-cache, force-cache
// Redirect mode
redirect: 'follow', // follow, error, manual
// Referrer policy
referrerPolicy: 'no-referrer', // no-referrer, origin, same-origin, strict-origin
// Request mode
mode: 'cors', // cors, no-cors, same-origin
// Signal (for cancellation)
signal: abortController.signal
});
Response Handling
Response Object
const response = await fetch(url);
// Response properties
console.log(response.ok); // true for 200-299
console.log(response.status); // HTTP status code
console.log(response.statusText); // Status text
console.log(response.url); // Final URL
console.log(response.headers); // Headers object
console.log(response.redirected); // Was redirected
// Read response headers
const contentType = response.headers.get('Content-Type');
const allHeaders = [...response.headers.entries()];
Response Body Parsing
// JSON
const jsonData = await response.json();
// Text
const textData = await response.text();
// Blob (binary)
const blobData = await response.blob();
const imageUrl = URL.createObjectURL(blobData);
// ArrayBuffer
const bufferData = await response.arrayBuffer();
// FormData
const formData = await response.formData();
// Note: body can only be read once
// Clone if needed multiple times
const clone = response.clone();
const data1 = await response.json();
const data2 = await clone.json();
Streaming Read
const response = await fetch(url);
const reader = response.body?.getReader();
if (reader) {
const decoder = new TextDecoder();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += decoder.decode(value, { stream: true });
console.log('Received chunk:', result.length);
}
console.log('Complete:', result);
}
Request Cancellation
AbortController
const controller = new AbortController();
// Make request
const fetchPromise = fetch(url, {
signal: controller.signal
});
// Cancel after timeout
const timeoutId = setTimeout(() => {
controller.abort();
}, 5000);
try {
const response = await fetchPromise;
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.log('Request was cancelled');
} else {
throw error;
}
}
Timeout Wrapper
async function fetchWithTimeout(
url: string,
options: RequestInit = {},
timeout = 5000
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
return response;
} finally {
clearTimeout(timeoutId);
}
}
// Usage
try {
const response = await fetchWithTimeout('/api/data', {}, 3000);
const data = await response.json();
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.log('Request timed out');
}
}
HTTP Client Wrapper
Basic Wrapper
interface RequestOptions extends Omit<RequestInit, 'body'> {
params?: Record<string, string>;
data?: unknown;
timeout?: number;
}
interface ApiResponse<T> {
data: T;
status: number;
headers: Headers;
}
class HttpClient {
private baseURL: string;
private defaultHeaders: HeadersInit;
constructor(baseURL: string, defaultHeaders: HeadersInit = {}) {
this.baseURL = baseURL;
this.defaultHeaders = defaultHeaders;
}
private buildURL(endpoint: string, params?: Record<string, string>): string {
const url = new URL(endpoint, this.baseURL);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
return url.toString();
}
async request<T>(
endpoint: string,
options: RequestOptions = {}
): Promise<ApiResponse<T>> {
const { params, data, timeout = 10000, ...init } = options;
const url = this.buildURL(endpoint, params);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...init,
headers: {
'Content-Type': 'application/json',
...this.defaultHeaders,
...init.headers
},
body: data ? JSON.stringify(data) : undefined,
signal: controller.signal
});
if (!response.ok) {
throw new HttpError(response.status, response.statusText);
}
const responseData = await response.json();
return {
data: responseData,
status: response.status,
headers: response.headers
};
} finally {
clearTimeout(timeoutId);
}
}
get<T>(endpoint: string, options?: RequestOptions) {
return this.request<T>(endpoint, { ...options, method: 'GET' });
}
post<T>(endpoint: string, data: unknown, options?: RequestOptions) {
return this.request<T>(endpoint, { ...options, method: 'POST', data });
}
put<T>(endpoint: string, data: unknown, options?: RequestOptions) {
return this.request<T>(endpoint, { ...options, method: 'PUT', data });
}
delete<T>(endpoint: string, options?: RequestOptions) {
return this.request<T>(endpoint, { ...options, method: 'DELETE' });
}
}
class HttpError extends Error {
constructor(
public status: number,
public statusText: string
) {
super(`HTTP ${status}: ${statusText}`);
this.name = 'HttpError';
}
}
Interceptor Pattern
type Interceptor<T> = (value: T) => T | Promise<T>;
interface Interceptors {
request: Interceptor<RequestInit>[];
response: Interceptor<Response>[];
}
class HttpClientWithInterceptors extends HttpClient {
private interceptors: Interceptors = {
request: [],
response: []
};
addRequestInterceptor(interceptor: Interceptor<RequestInit>) {
this.interceptors.request.push(interceptor);
}
addResponseInterceptor(interceptor: Interceptor<Response>) {
this.interceptors.response.push(interceptor);
}
async request<T>(
endpoint: string,
options: RequestOptions = {}
): Promise<ApiResponse<T>> {
// Apply request interceptors
let config: RequestInit = { ...options };
for (const interceptor of this.interceptors.request) {
config = await interceptor(config);
}
// Make request
let response = await fetch(endpoint, config);
// Apply response interceptors
for (const interceptor of this.interceptors.response) {
response = await interceptor(response);
}
const data = await response.json();
return { data, status: response.status, headers: response.headers };
}
}
// Using interceptors
const client = new HttpClientWithInterceptors('https://api.example.com');
// Add auth header
client.addRequestInterceptor(async (config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`
};
}
return config;
});
// Handle 401 errors
client.addResponseInterceptor(async (response) => {
if (response.status === 401) {
// Refresh token or redirect to login
localStorage.removeItem('token');
window.location.href = '/login';
}
return response;
});
Common Scenarios
File Upload
async function uploadFile(file: File, onProgress?: (percent: number) => void) {
const formData = new FormData();
formData.append('file', file);
// Basic upload
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
// Don't set Content-Type, browser handles it
});
return response.json();
}
// Multiple file upload
async function uploadMultiple(files: FileList) {
const formData = new FormData();
Array.from(files).forEach((file, index) => {
formData.append(`file${index}`, file);
});
return fetch('/api/upload-multiple', {
method: 'POST',
body: formData
});
}
File Download
async function downloadFile(url: string, filename: string) {
const response = await fetch(url);
const blob = await response.blob();
// Create download link
const downloadUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Cleanup URL
URL.revokeObjectURL(downloadUrl);
}
// Download with progress
async function downloadWithProgress(
url: string,
onProgress: (percent: number) => void
) {
const response = await fetch(url);
const contentLength = response.headers.get('Content-Length');
const total = contentLength ? parseInt(contentLength, 10) : 0;
const reader = response.body?.getReader();
if (!reader) throw new Error('No reader available');
const chunks: Uint8Array[] = [];
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
if (total > 0) {
onProgress((received / total) * 100);
}
}
return new Blob(chunks);
}
Concurrent Requests
// Promise.all - all succeed or any fails
async function fetchAll<T>(urls: string[]): Promise<T[]> {
const responses = await Promise.all(
urls.map(url => fetch(url))
);
return Promise.all(
responses.map(res => res.json())
);
}
// Promise.allSettled - get all results
async function fetchAllSettled<T>(urls: string[]) {
const results = await Promise.allSettled(
urls.map(async url => {
const res = await fetch(url);
return res.json();
})
);
return results.map(result => {
if (result.status === 'fulfilled') {
return { success: true, data: result.value };
}
return { success: false, error: result.reason };
});
}
// Limit concurrency
async function fetchWithConcurrency<T>(
urls: string[],
concurrency: number
): Promise<T[]> {
const results: T[] = [];
const executing: Promise<void>[] = [];
for (const url of urls) {
const promise = fetch(url)
.then(res => res.json())
.then(data => { results.push(data); });
executing.push(promise);
if (executing.length >= concurrency) {
await Promise.race(executing);
executing.splice(
executing.findIndex(p => p === promise),
1
);
}
}
await Promise.all(executing);
return results;
}
Best Practices Summary
Fetch API Best Practices:
┌─────────────────────────────────────────────────────┐
│ │
│ Error Handling │
│ ├── Check response.ok │
│ ├── Handle network errors │
│ ├── Handle timeouts │
│ └── Provide meaningful error messages │
│ │
│ Performance │
│ ├── Use request cancellation │
│ ├── Implement request caching │
│ ├── Control concurrency │
│ └── Use keep-alive │
│ │
│ Code Organization │
│ ├── Wrap in HTTP client │
│ ├── Use interceptors │
│ ├── Ensure type safety │
│ └── Centralize error handling │
│ │
└─────────────────────────────────────────────────────┘
| Method | Purpose |
|---|---|
| response.json() | Parse JSON |
| response.text() | Get text |
| response.blob() | Get binary |
| response.clone() | Clone response |
Master the Fetch API for more elegant HTTP requests.