javascript-today

Fetch API and HTTP Requests

The Fetch API provides a modern way to make HTTP requests in the browser. It replaces the older XMLHttpRequest with a cleaner, promise-based interface.

Basic GET Request

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

With async/await (cleaner):

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

getUsers();

Understanding the Response

async function checkResponse() {
  const response = await fetch('https://api.example.com/users');
  
  console.log('Status:', response.status);        // 200, 404, 500, etc.
  console.log('OK:', response.ok);                // true if 200-299
  console.log('Status text:', response.statusText); // "OK", "Not Found"
  console.log('Headers:', response.headers);
  console.log('URL:', response.url);
  
  // Parse body
  const data = await response.json();
}

Response Parsing Methods

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

// Plain text
const text = await response.text();

// HTML
const html = await response.text();

// Binary data (Blob)
const blob = await response.blob();

// Form data
const formData = await response.formData();

// Array buffer
const buffer = await response.arrayBuffer();

Error Handling

Fetch only rejects on network errors, not HTTP errors (404, 500):

async function fetchWithErrorHandling(url) {
  try {
    const response = await fetch(url);
    
    // Check if request succeeded
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    
    const data = await response.json();
    return data;
  } catch (error) {
    // Network error or response parsing error
    console.error('Fetch error:', error);
    throw error;
  }
}

// Usage
try {
  const users = await fetchWithErrorHandling('/api/users');
  console.log(users);
} catch (error) {
  // Handle error (show message to user, etc.)
}

POST Request (Creating Data)

async function createUser(userData) {
  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}`);
  }
  
  return await response.json();
}

// Usage
const newUser = await createUser({
  name: 'Alice',
  email: 'alice@example.com'
});
console.log('Created:', newUser);

PUT Request (Updating Data)

async function updateUser(userId, updates) {
  const response = await fetch(`https://api.example.com/users/${userId}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(updates)
  });
  
  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }
  
  return await response.json();
}

// Usage
await updateUser(123, { name: 'Alice Updated' });

DELETE Request

async function deleteUser(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`, {
    method: 'DELETE'
  });
  
  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }
  
  // Some APIs return empty response
  if (response.status === 204) {
    return null;
  }
  
  return await response.json();
}

Request Headers

async function fetchWithHeaders() {
  const response = await fetch('https://api.example.com/data', {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer YOUR_TOKEN_HERE',
      'X-Custom-Header': 'custom-value'
    }
  });
  
  return await response.json();
}

Query Parameters

// Manual URL construction
const params = new URLSearchParams({
  search: 'nodejs',
  page: 2,
  limit: 20
});

const url = `https://api.example.com/search?${params}`;
// https://api.example.com/search?search=nodejs&page=2&limit=20

const response = await fetch(url);
const data = await response.json();

Timeouts

Fetch doesn’t have built-in timeout, but you can add one:

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) {
    if (error.name === 'AbortError') {
      throw new Error('Request timeout');
    }
    throw error;
  }
}

// Usage
try {
  const data = await fetchWithTimeout('/api/slow-endpoint', 3000);
} catch (error) {
  console.error(error.message); // "Request timeout"
}

Canceling Requests

const controller = new AbortController();

// Start fetch
fetch('https://api.example.com/data', {
  signal: controller.signal
})
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Request canceled');
    }
  });

// Cancel it
controller.abort();

Useful for search-as-you-type:

let currentController = null;

async function searchAsYouType(query) {
  // Cancel previous request
  if (currentController) {
    currentController.abort();
  }
  
  // Create new controller
  currentController = new AbortController();
  
  try {
    const response = await fetch(`/api/search?q=${query}`, {
      signal: currentController.signal
    });
    const results = await response.json();
    displayResults(results);
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('Search error:', error);
    }
  }
}

// Each keystroke cancels previous request
searchInput.addEventListener('input', (e) => {
  searchAsYouType(e.target.value);
});

Working with FormData

Sending form data (including files):

async function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);
  formData.append('description', 'My file');
  
  const response = await fetch('/api/upload', {
    method: 'POST',
    body: formData  // Don't set Content-Type header!
  });
  
  return await response.json();
}

// Usage with file input
const fileInput = document.querySelector('#file-input');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (file) {
    const result = await uploadFile(file);
    console.log('Uploaded:', result);
  }
});

CORS (Cross-Origin Requests)

Browsers block cross-origin requests by default:

// This may fail if server doesn't allow CORS
fetch('https://api.other-domain.com/data')
  .catch(error => {
    // TypeError: Failed to fetch
    // Blocked by CORS policy
  });

Solutions:

  1. Server adds CORS headers (backend fix)
  2. Use a proxy (your server fetches it)
  3. JSONP (old technique, rarely used now)

Credentials and Cookies

By default, fetch doesn’t send cookies:

// Include cookies in request
fetch('https://api.example.com/user', {
  credentials: 'include'  // Send cookies
})

// Options:
// 'omit'      - never send cookies (default)
// 'same-origin' - send cookies for same origin only
// 'include'   - always send cookies

Real-World API Wrapper

class API {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    
    const config = {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      }
    };
    
    // Add auth token if available
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    
    try {
      const response = await fetch(url, config);
      
      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message || `HTTP ${response.status}`);
      }
      
      // Handle empty responses
      if (response.status === 204) {
        return null;
      }
      
      return await response.json();
    } catch (error) {
      console.error('API Error:', error);
      throw error;
    }
  }
  
  get(endpoint) {
    return this.request(endpoint);
  }
  
  post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
  
  put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }
  
  delete(endpoint) {
    return this.request(endpoint, {
      method: 'DELETE'
    });
  }
}

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

// GET request
const users = await api.get('/users');

// POST request
const newUser = await api.post('/users', {
  name: 'Alice',
  email: 'alice@example.com'
});

// PUT request
await api.put('/users/123', { name: 'Alice Updated' });

// DELETE request
await api.delete('/users/123');

Loading States

Show loading indicators:

async function loadUsers() {
  const loadingEl = document.querySelector('#loading');
  const usersEl = document.querySelector('#users');
  const errorEl = document.querySelector('#error');
  
  // Show loading
  loadingEl.style.display = 'block';
  errorEl.style.display = 'none';
  
  try {
    const response = await fetch('/api/users');
    
    if (!response.ok) {
      throw new Error('Failed to load users');
    }
    
    const users = await response.json();
    
    // Hide loading, show users
    loadingEl.style.display = 'none';
    usersEl.innerHTML = users.map(u => `<div>${u.name}</div>`).join('');
    
  } catch (error) {
    // Hide loading, show error
    loadingEl.style.display = 'none';
    errorEl.style.display = 'block';
    errorEl.textContent = error.message;
  }
}

Best Practices

DO:

  • Always handle errors
  • Check response.ok before parsing
  • Use async/await for cleaner code
  • Set appropriate headers
  • Use AbortController for cancellable requests
  • Show loading states to users

DON’T:

  • Assume fetch succeeded (check response.ok)
  • Forget error handling
  • Make requests in loops (use Promise.all)
  • Expose API keys in client code
  • Trust user input without validation

Summary

Task Code
GET request fetch(url).then(r => r.json())
POST request fetch(url, { method: 'POST', body: ... })
With headers fetch(url, { headers: { ... } })
Error check if (!response.ok) throw new Error()
Parse JSON await response.json()
Timeout Use AbortController with setTimeout
Cancel controller.abort()