🌐 Fetch API

Modern Way to Make HTTP Requests

What is the Fetch API?

The Fetch API provides a modern, promise-based interface for making HTTP requests. It's the replacement for XMLHttpRequest (XHR).

🚀 Basic Usage

Simple GET Request

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

// With async/await
async function getUsers() {
    try {
        let response = await fetch('https://api.example.com/users');
        let data = await response.json();
        console.log(data);
    } catch (error) {
        console.error('Error:', error);
    }
}

// Fetch returns a promise
let promise = fetch('/api/data');
console.log(promise);  // Promise {  }

Response Object

async function example() {
    let response = await fetch('/api/users');
    
    // Response properties
    console.log(response.ok);          // true if 200-299
    console.log(response.status);      // 200, 404, 500, etc.
    console.log(response.statusText);  // 'OK', 'Not Found', etc.
    console.log(response.url);         // Final URL (after redirects)
    console.log(response.headers);     // Headers object
    console.log(response.redirected);  // true if redirected
    console.log(response.type);        // 'basic', 'cors', 'opaque'
    
    // Check if successful
    if (response.ok) {
        let data = await response.json();
        console.log(data);
    } else {
        console.error('HTTP error:', response.status);
    }
}

📄 Reading Response Body

// JSON data (most common)
let response = await fetch('/api/users');
let data = await response.json();
console.log(data);

// Plain text
let response = await fetch('/api/text');
let text = await response.text();
console.log(text);

// Blob (images, files)
let response = await fetch('/images/photo.jpg');
let blob = await response.blob();
let img = document.createElement('img');
img.src = URL.createObjectURL(blob);

// ArrayBuffer (binary data)
let response = await fetch('/api/binary');
let buffer = await response.arrayBuffer();

// FormData
let response = await fetch('/api/form');
let formData = await response.formData();

// Important: Body can only be read once
let response = await fetch('/api/users');
let data1 = await response.json();
let data2 = await response.json();  // Error: body already consumed

// Clone response if needed
let response = await fetch('/api/users');
let clone = response.clone();
let data1 = await response.json();
let data2 = await clone.json();  // Works

⚙️ Request Options

HTTP Methods

// GET (default)
fetch('/api/users');

// POST
fetch('/api/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        name: 'John',
        email: 'john@example.com'
    })
});

// PUT
fetch('/api/users/1', {
    method: 'PUT',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        name: 'John Doe',
        email: 'john@example.com'
    })
});

// PATCH
fetch('/api/users/1', {
    method: 'PATCH',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        email: 'newemail@example.com'
    })
});

// DELETE
fetch('/api/users/1', {
    method: 'DELETE'
});

Headers

// Set headers
fetch('/api/data', {
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token123',
        'X-Custom-Header': 'value'
    }
});

// Headers object
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Authorization', 'Bearer token123');

fetch('/api/data', { headers });

// Read response headers
let response = await fetch('/api/data');
console.log(response.headers.get('Content-Type'));
console.log(response.headers.get('Date'));

// Iterate headers
for (let [key, value] of response.headers) {
    console.log(`${key}: ${value}`);
}

// Check if header exists
if (response.headers.has('Content-Type')) {
    console.log('Has Content-Type header');
}

Request Body

// JSON body
fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'John', age: 30 })
});

// FormData (multipart/form-data)
let formData = new FormData();
formData.append('name', 'John');
formData.append('avatar', fileInput.files[0]);

fetch('/api/upload', {
    method: 'POST',
    body: formData  // Content-Type set automatically
});

// URLSearchParams (application/x-www-form-urlencoded)
let params = new URLSearchParams();
params.append('name', 'John');
params.append('age', '30');

fetch('/api/form', {
    method: 'POST',
    body: params
});

// Plain text
fetch('/api/text', {
    method: 'POST',
    headers: { 'Content-Type': 'text/plain' },
    body: 'Hello, server!'
});

// Blob
let blob = new Blob(['Hello'], { type: 'text/plain' });
fetch('/api/blob', {
    method: 'POST',
    body: blob
});

Other Options

fetch('/api/data', {
    method: 'GET',
    
    // Credentials (cookies)
    credentials: 'include',  // Send cookies cross-origin
    // credentials: 'same-origin',  // Only same origin (default)
    // credentials: 'omit',  // Never send cookies
    
    // Cache
    cache: 'default',  // Use browser cache
    // cache: 'no-cache',  // Bypass cache
    // cache: 'reload',  // Force network
    // cache: 'force-cache',  // Use cache, even if stale
    
    // Redirect
    redirect: 'follow',  // Follow redirects (default)
    // redirect: 'error',  // Error on redirect
    // redirect: 'manual',  // Handle redirects manually
    
    // Referrer
    referrer: 'about:client',
    
    // Mode
    mode: 'cors',  // Cross-origin (default)
    // mode: 'no-cors',  // Limited response
    // mode: 'same-origin',  // Only same origin
    
    // Integrity (Subresource Integrity)
    integrity: 'sha256-...'
});

🛡️ Error Handling

// fetch() only rejects on network errors
fetch('/api/users')
    .then(response => {
        // HTTP errors (404, 500) don't throw!
        console.log(response.ok);  // false for 4xx, 5xx
    })
    .catch(error => {
        // Only network errors reach here
        console.error('Network error:', error);
    });

// Check response.ok
async function fetchData() {
    try {
        let response = await fetch('/api/users');
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        let data = await response.json();
        return data;
    } catch (error) {
        console.error('Error:', error.message);
        throw error;
    }
}

// Handle specific status codes
async function example() {
    let response = await fetch('/api/users');
    
    if (response.status === 404) {
        console.log('Not found');
    } else if (response.status === 401) {
        console.log('Unauthorized');
        redirectToLogin();
    } else if (response.status === 500) {
        console.log('Server error');
    } else if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
    }
    
    return response.json();
}

// Parse error response
async function handleError() {
    try {
        let response = await fetch('/api/users', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ name: '' })
        });
        
        if (!response.ok) {
            let error = await response.json();
            throw new Error(error.message || 'Request failed');
        }
        
        return response.json();
    } catch (error) {
        console.error('Error:', error.message);
    }
}

⏱️ Timeout and Abort

AbortController

// Abort fetch request
let controller = new AbortController();

fetch('/api/data', {
    signal: controller.signal
})
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => {
        if (error.name === 'AbortError') {
            console.log('Fetch aborted');
        } else {
            console.error('Error:', error);
        }
    });

// Abort after some time
setTimeout(() => controller.abort(), 5000);

// Abort on user action
let abortBtn = document.querySelector('#abortBtn');
abortBtn.addEventListener('click', () => {
    controller.abort();
});

Timeout Implementation

// Fetch with timeout
async function fetchWithTimeout(url, timeout = 5000) {
    let controller = new AbortController();
    let id = setTimeout(() => controller.abort(), timeout);
    
    try {
        let response = await fetch(url, {
            signal: controller.signal
        });
        clearTimeout(id);
        return response;
    } catch (error) {
        clearTimeout(id);
        if (error.name === 'AbortError') {
            throw new Error('Request timeout');
        }
        throw error;
    }
}

// Usage
try {
    let response = await fetchWithTimeout('/api/data', 3000);
    let data = await response.json();
    console.log(data);
} catch (error) {
    console.error(error.message);
}

// Reusable utility
function withTimeout(promise, timeout) {
    return Promise.race([
        promise,
        new Promise((_, reject) =>
            setTimeout(() => reject(new Error('Timeout')), timeout)
        )
    ]);
}

let response = await withTimeout(fetch('/api/data'), 5000);

💡 Practical Examples

API Client Class

class API {
    constructor(baseURL, defaultHeaders = {}) {
        this.baseURL = baseURL;
        this.defaultHeaders = defaultHeaders;
    }
    
    async request(endpoint, options = {}) {
        let url = `${this.baseURL}${endpoint}`;
        
        let config = {
            ...options,
            headers: {
                ...this.defaultHeaders,
                ...options.headers
            }
        };
        
        let response = await fetch(url, config);
        
        if (!response.ok) {
            let error = await response.json().catch(() => ({}));
            throw new Error(error.message || `HTTP ${response.status}`);
        }
        
        return response.json();
    }
    
    get(endpoint, options) {
        return this.request(endpoint, { ...options, method: 'GET' });
    }
    
    post(endpoint, data, options) {
        return this.request(endpoint, {
            ...options,
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });
    }
    
    put(endpoint, data, options) {
        return this.request(endpoint, {
            ...options,
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });
    }
    
    delete(endpoint, options) {
        return this.request(endpoint, { ...options, method: 'DELETE' });
    }
}

// Usage
let api = new API('https://api.example.com', {
    'Authorization': 'Bearer token123'
});

let users = await api.get('/users');
let newUser = await api.post('/users', { name: 'John' });
await api.delete('/users/1');

File Upload

async function uploadFile(file) {
    let formData = new FormData();
    formData.append('file', file);
    formData.append('name', file.name);
    
    try {
        let response = await fetch('/api/upload', {
            method: 'POST',
            body: formData
        });
        
        if (!response.ok) {
            throw new Error('Upload failed');
        }
        
        let result = await response.json();
        console.log('Uploaded:', result.url);
        return result;
    } catch (error) {
        console.error('Upload error:', error);
        throw error;
    }
}

// With progress
async function uploadWithProgress(file, onProgress) {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest();
        
        xhr.upload.addEventListener('progress', e => {
            if (e.lengthComputable) {
                let percent = (e.loaded / e.total) * 100;
                onProgress(percent);
            }
        });
        
        xhr.addEventListener('load', () => {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(JSON.parse(xhr.responseText));
            } else {
                reject(new Error(`HTTP ${xhr.status}`));
            }
        });
        
        xhr.addEventListener('error', () => {
            reject(new Error('Upload failed'));
        });
        
        let formData = new FormData();
        formData.append('file', file);
        
        xhr.open('POST', '/api/upload');
        xhr.send(formData);
    });
}

// Usage
let fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async () => {
    let file = fileInput.files[0];
    try {
        await uploadWithProgress(file, percent => {
            console.log(`Upload: ${percent.toFixed(0)}%`);
        });
        console.log('Complete!');
    } catch (error) {
        console.error('Failed:', error);
    }
});

Pagination

async function* fetchAllPages(baseUrl) {
    let page = 1;
    let hasMore = true;
    
    while (hasMore) {
        let response = await fetch(`${baseUrl}?page=${page}`);
        let data = await response.json();
        
        yield data.items;
        
        hasMore = data.hasMore;
        page++;
    }
}

// Usage
for await (let items of fetchAllPages('/api/users')) {
    console.log('Page:', items.length);
    processItems(items);
}

// Or load all at once
async function loadAll(baseUrl) {
    let allItems = [];
    
    for await (let items of fetchAllPages(baseUrl)) {
        allItems.push(...items);
    }
    
    return allItems;
}

let users = await loadAll('/api/users');

Retry Logic

async function fetchWithRetry(url, options = {}, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            let response = await fetch(url, options);
            
            if (response.ok || response.status === 404) {
                return response;
            }
            
            if (i === retries - 1) {
                throw new Error(`HTTP ${response.status}`);
            }
            
        } catch (error) {
            if (i === retries - 1) {
                throw error;
            }
            
            // Exponential backoff
            let delay = Math.pow(2, i) * 1000;
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
}

// Usage
try {
    let response = await fetchWithRetry('/api/flaky-endpoint', {}, 3);
    let data = await response.json();
    console.log(data);
} catch (error) {
    console.error('Failed after retries:', error);
}

Parallel Requests

// Load multiple resources
async function loadDashboard() {
    try {
        let [users, posts, stats] = await Promise.all([
            fetch('/api/users').then(r => r.json()),
            fetch('/api/posts').then(r => r.json()),
            fetch('/api/stats').then(r => r.json())
        ]);
        
        return { users, posts, stats };
    } catch (error) {
        console.error('Dashboard load failed:', error);
        throw error;
    }
}

// With error handling for each
async function loadDashboardSafe() {
    let results = await Promise.allSettled([
        fetch('/api/users').then(r => r.json()),
        fetch('/api/posts').then(r => r.json()),
        fetch('/api/stats').then(r => r.json())
    ]);
    
    let [usersResult, postsResult, statsResult] = results;
    
    return {
        users: usersResult.status === 'fulfilled' ? usersResult.value : [],
        posts: postsResult.status === 'fulfilled' ? postsResult.value : [],
        stats: statsResult.status === 'fulfilled' ? statsResult.value : {}
    };
}

Request Caching

class CachedFetch {
    constructor(ttl = 60000) {
        this.cache = new Map();
        this.ttl = ttl;
    }
    
    async fetch(url, options = {}) {
        let key = `${url}:${JSON.stringify(options)}`;
        
        if (this.cache.has(key)) {
            let { data, timestamp } = this.cache.get(key);
            if (Date.now() - timestamp < this.ttl) {
                return data;
            }
        }
        
        let response = await fetch(url, options);
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        let data = await response.json();
        
        this.cache.set(key, {
            data,
            timestamp: Date.now()
        });
        
        return data;
    }
    
    clear(url) {
        if (url) {
            for (let key of this.cache.keys()) {
                if (key.startsWith(url)) {
                    this.cache.delete(key);
                }
            }
        } else {
            this.cache.clear();
        }
    }
}

// Usage
let cached = new CachedFetch(30000);  // 30 second TTL

let users = await cached.fetch('/api/users');
let usersAgain = await cached.fetch('/api/users');  // From cache

cached.clear('/api/users');  // Clear cache

🎯 Key Takeaways