Home Module 14 Fetch API

1. Introduction

The Fetch API is the modern, Promise-based way to make HTTP requests from the browser. It replaces the older XMLHttpRequest (XHR) with a cleaner interface. You use fetch() to request data from APIs, send form data, upload files, and communicate with any HTTP server.

You will use fetch constantly as a frontend developer — it is how your JavaScript talks to backends and third-party services.

2. Theory

2.1 Basic GET request

// fetch returns a Promise that resolves to a Response object
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');

console.log(response.status);     // 200
console.log(response.ok);         // true (status 200-299)
console.log(response.statusText); // 'OK'
console.log(response.headers.get('content-type')); // 'application/json...'

// The Response body must be parsed separately
const user = await response.json(); // parse JSON body
console.log(user.name); // 'Leanne Graham'

// Other body parsers:
// response.text()       — plain text or HTML
// response.blob()       — binary data (images, files)
// response.formData()   — form data
// response.arrayBuffer() — raw binary

2.2 The critical mistake — fetch only rejects on network failure

// IMPORTANT: fetch does NOT reject on HTTP error status (404, 500, etc.)
// It only rejects for network errors (no internet, DNS failure)

// Bug — assumes rejection on 404
try {
  const user = await fetch('/api/users/999').then(r => r.json());
  console.log(user); // {} or error object — not caught!
} catch (err) {
  // Does NOT run for 404!
}

// Fix — always check response.ok
async function getUser(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  return response.json();
}

2.3 POST request — sending JSON

async function createUser(userData) {
  const response = await fetch('https://jsonplaceholder.typicode.com/users', {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify(userData)
  });

  if (!response.ok) throw new Error(`Failed: ${response.status}`);
  return response.json(); // server returns the created resource
}

const newUser = await createUser({
  name:  'Alice Johnson',
  email: 'alice@example.com',
  role:  'user'
});
console.log(newUser.id); // assigned by server

2.4 PUT and DELETE

// PUT — replace/update a resource
async function updateUser(id, data) {
  const response = await fetch(`/api/users/${id}`, {
    method:  'PUT',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify(data)
  });
  if (!response.ok) throw new Error(`Update failed: ${response.status}`);
  return response.json();
}

// PATCH — partial update
async function patchUser(id, changes) {
  const response = await fetch(`/api/users/${id}`, {
    method:  'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify(changes)
  });
  if (!response.ok) throw new Error(`Patch failed: ${response.status}`);
  return response.json();
}

// DELETE
async function deleteUser(id) {
  const response = await fetch(`/api/users/${id}`, { method: 'DELETE' });
  if (!response.ok) throw new Error(`Delete failed: ${response.status}`);
  // 204 No Content — response body may be empty
  return response.status === 204 ? null : response.json();
}

2.5 Request headers — authentication

// Bearer token authentication
const token = localStorage.getItem('authToken');

const response = await fetch('/api/protected', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type':  'application/json'
  }
});

// API key in header
const response2 = await fetch('https://api.weather.com/forecast', {
  headers: { 'X-API-Key': 'your-api-key-here' }
});

2.6 Query parameters

// Build URLs with query params using URLSearchParams
const params = new URLSearchParams({
  search: 'javascript',
  page:   1,
  limit:  10,
  sort:   'recent'
});

const url = `https://api.example.com/posts?${params}`;
// https://api.example.com/posts?search=javascript&page=1&limit=10&sort=recent

const response = await fetch(url);
const { posts, total } = await response.json();

2.7 Aborting requests with AbortController

// Cancel a fetch when user navigates away or types a new search
let controller = null;

async function search(query) {
  // Abort previous request if still pending
  if (controller) controller.abort();
  controller = new AbortController();

  try {
    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: controller.signal
    });
    const data = await response.json();
    renderResults(data);
  } catch (err) {
    if (err.name === 'AbortError') return; // ignore — intentionally aborted
    console.error('Search failed:', err);
  }
}

document.querySelector('#search').addEventListener('input', e => {
  search(e.target.value);
});

2.8 A reusable fetch wrapper

// Build once, use everywhere
async function request(url, options = {}) {
  const token = sessionStorage.getItem('token');

  const config = {
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { 'Authorization': `Bearer ${token}` } : {}),
      ...options.headers
    },
    ...options
  };

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

  const response = await fetch(url, config);

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(error.message || `HTTP ${response.status}`);
  }

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

// Clean API module
const api = {
  get:    url         => request(url),
  post:   (url, body) => request(url, { method: 'POST',   body }),
  put:    (url, body) => request(url, { method: 'PUT',    body }),
  patch:  (url, body) => request(url, { method: 'PATCH',  body }),
  delete: url         => request(url, { method: 'DELETE' })
};

3. Real World Example

// Public JSONPlaceholder API — works right now in your browser!
const BASE = 'https://jsonplaceholder.typicode.com';

async function renderUserPosts(userId) {
  const container = document.querySelector('#posts');
  container.textContent = 'Loading...';

  try {
    // Fetch user and posts in parallel
    const [user, posts] = await Promise.all([
      fetch(`${BASE}/users/${userId}`).then(r => {
        if (!r.ok) throw new Error(`User not found (${r.status})`);
        return r.json();
      }),
      fetch(`${BASE}/posts?userId=${userId}`).then(r => r.json())
    ]);

    container.innerHTML = '';

    const h2 = document.createElement('h2');
    h2.textContent = `Posts by ${user.name}`;
    container.appendChild(h2);

    posts.slice(0, 5).forEach(post => {
      const article = document.createElement('article');
      const h3 = document.createElement('h3');
      const p  = document.createElement('p');
      h3.textContent = post.title;
      p.textContent  = post.body;
      article.append(h3, p);
      container.appendChild(article);
    });

  } catch (err) {
    container.textContent = 'Error: ' + err.message;
  }
}

renderUserPosts(1);

4. Code Example

<input id="user-id" type="number" min="1" max="10" value="1">
<button id="load">Load User</button>
<div id="output"></div>

<script>
  const BASE   = 'https://jsonplaceholder.typicode.com';
  const output = document.querySelector('#output');

  async function loadUser(id) {
    output.textContent = 'Loading...';

    try {
      // Parallel: user data + their todos
      const [userRes, todosRes] = await Promise.all([
        fetch(`${BASE}/users/${id}`),
        fetch(`${BASE}/todos?userId=${id}&_limit=5`)
      ]);

      if (!userRes.ok)  throw new Error(`User ${id} not found`);
      if (!todosRes.ok) throw new Error('Could not load todos');

      const [user, todos] = await Promise.all([
        userRes.json(),
        todosRes.json()
      ]);

      output.innerHTML = '';

      // User card — textContent for user data to prevent XSS
      const card = document.createElement('div');
      const name = document.createElement('h2');
      const info = document.createElement('p');
      name.textContent = user.name;
      info.textContent = `${user.email} — ${user.company.name}`;
      card.append(name, info);

      // Todos list
      const h3 = document.createElement('h3');
      h3.textContent = 'Recent Todos';
      const ul = document.createElement('ul');
      todos.forEach(todo => {
        const li = document.createElement('li');
        li.textContent = `${todo.completed ? '✓' : '○'} ${todo.title}`;
        li.style.color = todo.completed ? 'green' : 'inherit';
        ul.appendChild(li);
      });

      output.append(card, h3, ul);

    } catch (err) {
      output.textContent = 'Error: ' + err.message;
    }
  }

  document.querySelector('#load').addEventListener('click', () => {
    loadUser(Number(document.querySelector('#user-id').value));
  });

  loadUser(1); // initial load
</script>

5. Code Breakdown

Two-step JSON parsing

The first Promise.all awaits both Response objects. The second Promise.all awaits calling .json() on each — both bodies are parsed in parallel. This is more efficient than awaiting each sequentially.

Checking response.ok on each response

Each response is checked individually because the user request and todos request are independent — one might fail while the other succeeds. The guard throws with a meaningful message.

textContent for API data

All user-provided data (name, email, todo titles) is set via textContent, not innerHTML. API data is external and untrusted — always treat it as user input.

6. Common Mistakes

Mistake 1 — Not checking response.ok

// Bug — 404 and 500 responses resolve, not reject
const data = await fetch('/api/missing').then(r => r.json());
// data might be { error: 'Not found' } — no exception thrown

// Fix
const response = await fetch('/api/missing');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();

Mistake 2 — Reading the body twice

// Bug — response body can only be read once
const response = await fetch('/api/data');
const text = await response.text();
const json = await response.json(); // Error: body already consumed!

// Fix — read once in the right format
const response = await fetch('/api/data');
const json = await response.json();

Mistake 3 — Forgetting Content-Type for POST

// Bug — server may not parse the body without the header
await fetch('/api/users', {
  method: 'POST',
  body: JSON.stringify({ name: 'Alice' }) // server doesn't know it's JSON!
});

// Fix — add Content-Type header
await fetch('/api/users', {
  method:  'POST',
  headers: { 'Content-Type': 'application/json' },
  body:    JSON.stringify({ name: 'Alice' })
});

7. Best Practices

  1. Always check response.ok and throw on HTTP errors — fetch does not do this automatically.
  2. Always set Content-Type: application/json when sending a JSON body.
  3. Use a fetch wrapper to centralise auth headers, error handling, and JSON parsing.
  4. Use AbortController to cancel stale requests when the user navigates away or searches again.
  5. Use URLSearchParams to build query strings — it handles encoding automatically.
  6. Treat API data as untrusted — always use textContent, never innerHTML for API-sourced text.

8. Practice Exercise

  1. Fetch a user from https://jsonplaceholder.typicode.com/users/1 and display their name, email, phone, and website.
  2. Create a search box that fetches https://jsonplaceholder.typicode.com/posts?userId={id} when a user ID is entered. Use debouncing (wait 400ms after last input).
  3. Build a mini CRUD UI using JSONPlaceholder — display a list of posts (GET), add a new post (POST — it won't persist but the API responds), and delete a post (DELETE — same). Log each response.

9. Assignment

Build a "Movie Search App" using the OMDb API (free tier at omdbapi.com).

  1. Register for a free API key at omdbapi.com.
  2. Search field — on form submit, fetch http://www.omdbapi.com/?apikey=KEY&s=QUERY and display movie posters, titles, and years in a grid.
  3. Click a movie to fetch its full details http://www.omdbapi.com/?apikey=KEY&i=IMDBID and show them in a modal.
  4. Handle errors: no results found, network error, API error (response.Error field).
  5. Show a loading spinner during every fetch.

Deliverable: One HTML file.

10. Interview Questions

  1. What is the Fetch API?
    A Promise-based browser API for making HTTP requests. fetch(url, options) returns a Promise that resolves to a Response object. The response body must be separately parsed with .json(), .text(), or .blob().
  2. Does fetch reject on 404 errors?
    No. fetch only rejects on network failures (no connection, DNS failure). HTTP error responses (404, 500, etc.) resolve the Promise — you must check response.ok or response.status and throw manually.
  3. How do you send JSON data with fetch?
    Pass method: 'POST', a headers object with Content-Type: 'application/json', and a body of JSON.stringify(data).
  4. What is AbortController used for?
    It creates an AbortSignal that can be passed to fetch's signal option. Calling controller.abort() cancels the request — the fetch Promise rejects with an AbortError. Used to cancel stale search requests or cleanup on component unmount.

11. Additional Resources

  • MDN — Fetch API
  • MDN — Using Fetch — comprehensive guide
  • JSONPlaceholder — free fake REST API for testing
  • MDN — AbortController