Beginner
30 min
Full Guide

Fetch API & HTTP Requests

Master the Fetch API for making HTTP requests in JavaScript with practical examples

What is the Fetch API?

The Fetch API is a modern, promise-based interface for making HTTP requests in JavaScript. It's built into all modern browsers and provides a cleaner, more powerful alternative to the older XMLHttpRequest (XHR) approach.

Fetch is native to JavaScript—no external libraries required—and works seamlessly with async/await syntax.

✨ Fetch API Advantages

🎯
Promise-Based

Clean async/await syntax

📦
Native Support

No external dependencies

🔄
Streaming

Stream response body

🛡️
CORS Support

Built-in cross-origin handling

Basic GET Request

// Simple GET request
async function fetchUsers() {
  try {
    const response = await fetch('https://api.example.com/users');
    
    // Check if request was successful
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    
    // Parse JSON response
    const users = await response.json();
    console.log('Users:', users);
    return users;
    
  } catch (error) {
    console.error('Fetch error:', error);
  }
}

// With query parameters
async function searchUsers(query, page = 1) {
  const params = new URLSearchParams({
    q: query,
    page: page,
    limit: 10
  });
  
  const response = await fetch(`https://api.example.com/users?${params}`);
  return response.json();
}

// Using URL constructor
async function getUserById(id) {
  const url = new URL(`https://api.example.com/users/${id}`);
  url.searchParams.set('include', 'posts');
  
  const response = await fetch(url);
  return response.json();
}

POST Request - Creating Data

// POST request with JSON body
async function createUser(userData) {
  try {
    const response = await fetch('https://api.example.com/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer your-token-here'
      },
      body: JSON.stringify(userData)
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Failed to create user');
    }

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

// Usage
createUser({
  name: 'John Doe',
  email: 'john@example.com',
  role: 'admin'
});

// POST with form data
async function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);
  formData.append('description', 'Profile picture');

  const response = await fetch('https://api.example.com/upload', {
    method: 'POST',
    // Don't set Content-Type for FormData - browser sets it automatically
    body: formData
  });

  return response.json();
}

PUT, PATCH, and DELETE Requests

// PUT - Replace entire resource
async function updateUser(id, userData) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer your-token'
    },
    body: JSON.stringify(userData)
  });

  if (!response.ok) {
    throw new Error('Failed to update user');
  }

  return response.json();
}

// PATCH - Partial update
async function patchUser(id, updates) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer your-token'
    },
    body: JSON.stringify(updates)
  });

  return response.json();
}

// Usage: Only update specific fields
patchUser(123, { status: 'active' });

// DELETE - Remove resource
async function deleteUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: 'DELETE',
    headers: {
      'Authorization': 'Bearer your-token'
    }
  });

  if (!response.ok) {
    throw new Error('Failed to delete user');
  }

  // DELETE often returns 204 No Content
  if (response.status === 204) {
    return { success: true };
  }

  return response.json();
}

Request Configuration Options

// Full fetch options reference
const response = await fetch(url, {
  // HTTP method
  method: 'POST',
  
  // Request headers
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token',
    'Accept': 'application/json',
    'X-Custom-Header': 'value'
  },
  
  // Request body (for POST, PUT, PATCH)
  body: JSON.stringify(data),
  
  // Request mode
  mode: 'cors',          // cors, no-cors, same-origin
  
  // Credentials inclusion
  credentials: 'include', // include, same-origin, omit
  
  // Cache mode
  cache: 'default',      // default, no-store, reload, no-cache, force-cache
  
  // Redirect behavior
  redirect: 'follow',    // follow, error, manual
  
  // Referrer policy
  referrerPolicy: 'no-referrer-when-downgrade',
  
  // AbortController signal for cancellation
  signal: controller.signal,
  
  // Keep connection alive after page unload
  keepalive: false
});

Response Handling

// Response object properties and methods
async function handleResponse() {
  const response = await fetch('https://api.example.com/data');

  // Response properties
  console.log('Status:', response.status);         // 200
  console.log('Status Text:', response.statusText); // "OK"
  console.log('OK?:', response.ok);                // true (if 200-299)
  console.log('Headers:', response.headers);
  console.log('URL:', response.url);
  console.log('Type:', response.type);             // "cors", "basic", etc.
  console.log('Redirected:', response.redirected);

  // Reading headers
  const contentType = response.headers.get('Content-Type');
  const rateLimit = response.headers.get('X-RateLimit-Remaining');

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

  // Response body methods (can only be read ONCE!)
  // Choose the appropriate one based on content type:

  // For JSON
  const json = await response.json();

  // For plain text
  const text = await response.text();

  // For binary data (images, files)
  const blob = await response.blob();

  // For binary as ArrayBuffer
  const buffer = await response.arrayBuffer();

  // For form data
  const formData = await response.formData();
}

// Clone response to read body multiple times
async function readMultipleTimes() {
  const response = await fetch(url);
  
  // Clone before reading
  const clone = response.clone();
  
  const text = await response.text();
  const json = await clone.json(); // Can still read clone
}

Error Handling

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

    // Fetch only rejects on network errors, not HTTP errors!
    // We need to check response.ok manually
    
    if (!response.ok) {
      // Try to get error details from response body
      let errorMessage = `HTTP error! Status: ${response.status}`;
      
      try {
        const errorData = await response.json();
        errorMessage = errorData.message || errorMessage;
      } catch {
        // Response body wasn't JSON
      }

      // Create custom error with details
      const error = new Error(errorMessage);
      error.status = response.status;
      error.statusText = response.statusText;
      throw error;
    }

    return await response.json();
    
  } catch (error) {
    // Network errors (no internet, DNS failure, etc.)
    if (error.name === 'TypeError') {
      console.error('Network error:', error.message);
      throw new Error('Unable to connect to server');
    }
    
    // Abort errors
    if (error.name === 'AbortError') {
      console.error('Request was cancelled');
      throw new Error('Request cancelled');
    }
    
    // Re-throw other errors
    throw error;
  }
}

// Usage with specific error handling
async function loadData() {
  try {
    const data = await fetchWithErrorHandling('/api/data');
    console.log('Data loaded:', data);
  } catch (error) {
    if (error.status === 401) {
      // Redirect to login
      window.location.href = '/login';
    } else if (error.status === 404) {
      // Show not found message
      showNotFound();
    } else {
      // Generic error
      showError(error.message);
    }
  }
}

Request Cancellation with AbortController

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

  try {
    const response = await fetch(url, {
      signal: controller.signal
    });
    
    clearTimeout(timeoutId);
    return await response.json();
    
  } catch (error) {
    clearTimeout(timeoutId);
    
    if (error.name === 'AbortError') {
      throw new Error('Request timed out');
    }
    throw error;
  }
}

// Cancel on user action
class SearchController {
  constructor() {
    this.controller = null;
  }

  async search(query) {
    // Cancel previous request
    if (this.controller) {
      this.controller.abort();
    }

    // Create new controller for this request
    this.controller = new AbortController();

    try {
      const response = await fetch(`/api/search?q=${query}`, {
        signal: this.controller.signal
      });
      return await response.json();
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('Previous search cancelled');
        return null;
      }
      throw error;
    }
  }
}

// Usage with search input
const searchController = new SearchController();

searchInput.addEventListener('input', async (e) => {
  const results = await searchController.search(e.target.value);
  if (results) {
    displayResults(results);
  }
});

Parallel and Sequential Requests

// Parallel requests with Promise.all
async function fetchParallel() {
  try {
    const [users, posts, comments] = await Promise.all([
      fetch('/api/users').then(r => r.json()),
      fetch('/api/posts').then(r => r.json()),
      fetch('/api/comments').then(r => r.json())
    ]);

    console.log('Users:', users);
    console.log('Posts:', posts);
    console.log('Comments:', comments);
    
    return { users, posts, comments };
  } catch (error) {
    // One failure = all fail
    console.error('One request failed:', error);
  }
}

// Parallel with partial failure handling
async function fetchWithPartialSuccess() {
  const results = await Promise.allSettled([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json())
  ]);

  return results.map((result, index) => {
    if (result.status === 'fulfilled') {
      return { success: true, data: result.value };
    }
    return { success: false, error: result.reason };
  });
}

// Sequential requests (when order matters)
async function fetchSequential() {
  // Get user first
  const userResponse = await fetch('/api/users/1');
  const user = await userResponse.json();

  // Then get user's posts
  const postsResponse = await fetch(`/api/users/${user.id}/posts`);
  const posts = await postsResponse.json();

  // Then get comments for each post
  const postsWithComments = await Promise.all(
    posts.map(async (post) => {
      const commentsResponse = await fetch(`/api/posts/${post.id}/comments`);
      const comments = await commentsResponse.json();
      return { ...post, comments };
    })
  );

  return { user, posts: postsWithComments };
}

Reusable Fetch Wrapper

// Production-ready fetch wrapper
class HttpClient {
  constructor(baseUrl, defaultOptions = {}) {
    this.baseUrl = baseUrl;
    this.defaultOptions = {
      headers: {
        'Content-Type': 'application/json'
      },
      ...defaultOptions
    };
  }

  setAuthToken(token) {
    this.defaultOptions.headers['Authorization'] = `Bearer ${token}`;
  }

  removeAuthToken() {
    delete this.defaultOptions.headers['Authorization'];
  }

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

    if (options.body && typeof options.body === 'object') {
      mergedOptions.body = JSON.stringify(options.body);
    }

    const response = await fetch(url, mergedOptions);

    if (!response.ok) {
      const error = new Error('Request failed');
      error.status = response.status;
      try {
        error.data = await response.json();
      } catch {}
      throw error;
    }

    if (response.status === 204) {
      return null;
    }

    return response.json();
  }

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

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

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

  patch(endpoint, body, options) {
    return this.request(endpoint, { ...options, method: 'PATCH', body });
  }

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

// Usage
const api = new HttpClient('https://api.example.com/v1');
api.setAuthToken('jwt-token-here');

// Make requests
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'John', email: 'john@test.com' });
await api.patch(`/users/${newUser.id}`, { status: 'active' });
await api.delete(`/users/${newUser.id}`);

💡 Fetch API Best Practices

  • Always check response.ok - Fetch doesn't reject on HTTP errors
  • Use try/catch - Handle network errors and HTTP errors separately
  • Set appropriate headers - Content-Type, Authorization, etc.
  • Use AbortController - Cancel stale requests
  • Create reusable wrappers - DRY code with consistent error handling
  • Use URLSearchParams - For query string construction