4.8: Fetch API & AJAX

Understand asynchronous JavaScript, promises, and async/await for handling time-based operations and API calls. Learn how to fetch data from external sources and work with JSON to create dynamic, data-driven web applications.

1. What is AJAX?

AJAX (Asynchronous JavaScript And XML) allows updating parts of a web page without reloading the entire page.

Traditional Web (Without AJAX)

1. User clicks link
2. Browser sends request to server
3. Server sends back FULL HTML page
4. Browser reloads entire page
5. User sees new page

Problem: Slow, poor user experience, wastes bandwidth.

Modern Web (With AJAX)

1. User interacts with page
2. JavaScript sends request to server
3. Server sends back DATA (JSON)
4. JavaScript updates page dynamically
5. No page reload!

Examples:

  • Google Maps (pan without reload)
  • Gmail (check emails without refresh)
  • Facebook feed (infinite scroll)
  • Search autocomplete

2. The Fetch API

Fetch is the modern API for making HTTP requests (replaces old XMLHttpRequest).

Basic GET Request

fetch("https://jsonplaceholder.typicode.com/users/1")
  .then(response => response.json())  // Parse JSON
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.log("Error:", error);
  });

With async/await (cleaner):

async function getUser() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.log("Error:", error);
  }
}

getUser();

Python comparison (requests library):

import requests

response = requests.get("https://jsonplaceholder.typicode.com/users/1")
data = response.json()
print(data)

Response Object

const response = await fetch(url);

console.log(response.status);      // 200, 404, 500, etc.
console.log(response.ok);           // true if 200-299
console.log(response.statusText);   // "OK", "Not Found", etc.
console.log(response.headers);      // Response headers
console.log(response.url);          // Final URL (after redirects)

// Parse response body
const json = await response.json();      // JSON
const text = await response.text();      // Plain text
const blob = await response.blob();      // Binary (images, files)

Checking Response Status

async function fetchUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);

  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }

  return await response.json();
}

try {
  const user = await fetchUser(1);
  console.log(user);
} catch (error) {
  console.log("Error:", error.message);
}

Important: Fetch only rejects on network errors, not HTTP errors (404, 500). Always check response.ok.


3. HTTP Methods

GET - Retrieve Data

// Get all users
const response = await fetch("https://api.example.com/users");
const users = await response.json();

// Get specific user
const response = await fetch("https://api.example.com/users/1");
const user = await response.json();

POST - Create Data

const newUser = {
  name: "Alice",
  email: "alice@example.com"
};

const response = await fetch("https://api.example.com/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify(newUser)
});

const data = await response.json();
console.log("Created:", data);

Python comparison:

import requests
import json

new_user = {"name": "Alice", "email": "alice@example.com"}

response = requests.post(
    "https://api.example.com/users",
    json=new_user
)

data = response.json()

PUT - Update Data (Full Replace)

const updatedUser = {
  id: 1,
  name: "Alice Smith",
  email: "alice.smith@example.com"
};

const response = await fetch("https://api.example.com/users/1", {
  method: "PUT",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify(updatedUser)
});

const data = await response.json();
console.log("Updated:", data);

PATCH - Update Data (Partial)

const updates = {
  email: "newemail@example.com"
};

const response = await fetch("https://api.example.com/users/1", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify(updates)
});

DELETE - Remove Data

const response = await fetch("https://api.example.com/users/1", {
  method: "DELETE"
});

if (response.ok) {
  console.log("User deleted");
}

4. Request Options

Headers

const response = await fetch(url, {
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer your-token-here",
    "Accept": "application/json",
    "Custom-Header": "value"
  }
});

Request Body

// JSON
const response = await fetch(url, {
  method: "POST",
  body: JSON.stringify({ name: "Alice" })
});

// Form data
const formData = new FormData();
formData.append("name", "Alice");
formData.append("file", fileInput.files[0]);

const response = await fetch(url, {
  method: "POST",
  body: formData
  // Don't set Content-Type header (browser sets it automatically)
});

// URL encoded
const params = new URLSearchParams();
params.append("name", "Alice");
params.append("age", "30");

const response = await fetch(url, {
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded"
  },
  body: params
});

Other Options

const response = await fetch(url, {
  method: "GET",
  mode: "cors",           // cors, no-cors, same-origin
  cache: "no-cache",      // default, no-cache, reload, force-cache
  credentials: "include", // include, same-origin, omit
  redirect: "follow",     // follow, error, manual
  referrerPolicy: "no-referrer"
});

5. Handling Errors

Network Errors

async function fetchData(url) {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      console.log("Network error:", error.message);
    } else {
      console.log("Error:", error.message);
    }
    throw error;  // Re-throw if needed
  }
}

Timeout

Fetch doesn't have built-in timeout, use 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) {
    if (error.name === "AbortError") {
      throw new Error("Request timeout");
    }
    throw error;
  }
}

try {
  const data = await fetchWithTimeout("https://api.example.com/slow", 3000);
} catch (error) {
  console.log("Error:", error.message);
}

6. Working with JSON

JSON.stringify() - Object to JSON

const user = {
  name: "Alice",
  age: 30,
  active: true
};

const json = JSON.stringify(user);
console.log(json);  // '{"name":"Alice","age":30,"active":true}'

// Pretty print
const prettyJson = JSON.stringify(user, null, 2);
console.log(prettyJson);
/*
{
  "name": "Alice",
  "age": 30,
  "active": true
}
*/

JSON.parse() - JSON to Object

const jsonString = '{"name":"Alice","age":30}';
const user = JSON.parse(jsonString);
console.log(user.name);  // "Alice"

Handling Invalid JSON

async function fetchData(url) {
  const response = await fetch(url);

  const text = await response.text();  // Get as text first

  try {
    return JSON.parse(text);
  } catch (error) {
    console.log("Invalid JSON:", text);
    throw new Error("Invalid JSON response");
  }
}

7. Practical Examples

Example 1: Display User List

HTML:

<div id="users"></div>

JavaScript:

async function loadUsers() {
  const container = document.querySelector("#users");
  container.innerHTML = "Loading...";

  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    const users = await response.json();

    container.innerHTML = users.map(user => `
      <div class="user">
        <h3>${user.name}</h3>
        <p>${user.email}</p>
      </div>
    `).join("");
  } catch (error) {
    container.innerHTML = `<p>Error: ${error.message}</p>`;
  }
}

loadUsers();

Example 2: Search with Debounce

<input type="text" id="search" placeholder="Search users...">
<div id="results"></div>
function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func(...args), delay);
  };
}

async function searchUsers(query) {
  if (!query) {
    document.querySelector("#results").innerHTML = "";
    return;
  }

  try {
    const response = await fetch(`https://api.github.com/search/users?q=${query}`);
    const data = await response.json();

    document.querySelector("#results").innerHTML = data.items
      .slice(0, 5)
      .map(user => `<div>${user.login}</div>`)
      .join("");
  } catch (error) {
    console.log("Error:", error);
  }
}

const debouncedSearch = debounce(searchUsers, 500);

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

Example 3: Post Form Data

<form id="user-form">
  <input type="text" name="name" placeholder="Name" required>
  <input type="email" name="email" placeholder="Email" required>
  <button type="submit">Create User</button>
</form>
<div id="message"></div>
document.querySelector("#user-form").addEventListener("submit", async (e) => {
  e.preventDefault();

  const formData = new FormData(e.target);
  const user = Object.fromEntries(formData);

  const message = document.querySelector("#message");
  message.textContent = "Creating user...";

  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(user)
    });

    const data = await response.json();
    message.textContent = `User created with ID: ${data.id}`;
    e.target.reset();
  } catch (error) {
    message.textContent = `Error: ${error.message}`;
  }
});

Example 4: Loading Indicator

async function fetchWithLoading(url) {
  const loader = document.querySelector("#loader");
  const content = document.querySelector("#content");

  loader.style.display = "block";
  content.style.display = "none";

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

    content.innerHTML = JSON.stringify(data, null, 2);
    content.style.display = "block";
  } catch (error) {
    content.innerHTML = `Error: ${error.message}`;
    content.style.display = "block";
  } finally {
    loader.style.display = "none";
  }
}

Example 5: Retry Logic

async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url, options);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      console.log(`Attempt ${i + 1} failed:`, error.message);

      if (i === retries - 1) {
        throw error;  // Last attempt failed
      }

      // Wait before retry (exponential backoff)
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
}

try {
  const data = await fetchWithRetry("https://api.example.com/data");
  console.log(data);
} catch (error) {
  console.log("All retries failed:", error.message);
}

8. CORS (Cross-Origin Resource Sharing)

What is CORS?

Browser security feature that restricts requests to different domains.

Same-origin: ✅ Allowed

  • https://example.comhttps://example.com/api

Cross-origin: ❌ Blocked (unless server allows)

  • https://example.comhttps://api.other.com

CORS Error

Access to fetch at 'https://api.other.com' from origin 'https://example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header

Solutions

1. Server must enable CORS (backend fix):

// Server-side (Node.js/Express)
app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
  res.header("Access-Control-Allow-Headers", "Content-Type");
  next();
});

2. Use CORS proxy (development only):

const proxyUrl = "https://cors-anywhere.herokuapp.com/";
const apiUrl = "https://api.other.com/data";

const response = await fetch(proxyUrl + apiUrl);

3. Server-side request (Node.js, Python):

  • Make request from your server (no CORS restriction)
  • Your frontend requests your server

9. Practical Exercises

Exercise 4.8.1: GitHub User Finder

Build an app that:

  1. Takes username input
  2. Fetches GitHub user data
  3. Displays profile info and repos
  4. Handles errors (user not found)

API: https://api.github.com/users/{username}

Exercise 4.8.2: Todo API Client

Create a todo app using JSONPlaceholder API:

  1. Fetch and display todos
  2. Add new todo (POST)
  3. Mark todo complete (PATCH)
  4. Delete todo (DELETE)

API: https://jsonplaceholder.typicode.com/todos

Exercise 4.8.3: Weather App

Build a weather app:

  1. Get user location
  2. Fetch weather data
  3. Display current weather and forecast
  4. Handle loading and errors

API: OpenWeatherMap or similar

Create image search:

  1. Search query input
  2. Fetch images from Unsplash API
  3. Display results in grid
  4. Infinite scroll (load more)

10. Knowledge Check

Question 1: What does AJAX stand for and what does it do?

Show answer Asynchronous JavaScript And XML. Allows updating parts of a web page without full reload by fetching data from server asynchronously.

Question 2: What's the difference between response.json() and response.text()?

Show answer response.json() parses response as JSON and returns object. response.text() returns raw text string.

Question 3: When does fetch() reject?

Show answer Only on network errors. HTTP errors (404, 500) don't reject. Always check `response.ok` for HTTP status.

Question 4: How do you send JSON data in a POST request?

Show answer Set method to "POST", Content-Type header to "application/json", and body to JSON.stringify(data).

Question 5: What is CORS and why does it exist?

Show answer Cross-Origin Resource Sharing. Browser security feature that restricts cross-origin requests to prevent malicious sites from accessing user data.

11. Key Takeaways

  • AJAX enables dynamic updates without page reload
  • Fetch API is modern way to make HTTP requests
  • Always check response.ok before parsing (fetch doesn't reject on HTTP errors)
  • Use async/await for cleaner async code
  • HTTP methods: GET (retrieve), POST (create), PUT/PATCH (update), DELETE (remove)
  • Set Content-Type header to "application/json" when sending JSON
  • Use JSON.stringify() to convert object to JSON, JSON.parse() to parse JSON
  • Handle loading states and errors for better UX
  • CORS restricts cross-origin requests (server must allow)
  • Use AbortController for request timeout

12. Further Resources

Documentation:

Free APIs for Practice:


Next Steps

Excellent! You can now build data-driven applications with external APIs.

In Lesson 4.9: ES6+ Modern Features, you'll learn modern JavaScript features like destructuring, modules, optional chaining, and more.

Next: Lesson 4.9 - ES6+ Modern Features →