Introduction

Modern web applications rely heavily on APIs to fetch and send data. The Fetch API provides a powerful and flexible way to make HTTP requests in JavaScript. In this lesson, you'll learn how to interact with REST APIs, handle JSON data, work with different HTTP methods, and build robust API integrations.

Learning Objectives

By the end of this lesson, you will be able to:

  • Understand REST API concepts and HTTP methods
  • Use the Fetch API to make HTTP requests
  • Handle JSON data from API responses
  • Implement error handling for network requests
  • Work with query parameters and headers
  • Build a complete API integration

Prerequisites

  • Completion of Lessons 01-04
  • Understanding of async/await and Promises
  • Basic knowledge of HTTP and web protocols

Estimated Time

3.5 hours (including practice exercises)

Understanding REST APIs

REST (Representational State Transfer) is an architectural style for designing networked applications. REST APIs use HTTP methods to perform operations on resources.

HTTP Methods

  • GET - Retrieve data from the server
  • POST - Create new data on the server
  • PUT - Update existing data (replace entire resource)
  • PATCH - Update existing data (partial update)
  • DELETE - Remove data from the server

Common HTTP Status Codes

  • 200 OK - Request succeeded
  • 201 Created - Resource created successfully
  • 400 Bad Request - Invalid request data
  • 401 Unauthorized - Authentication required
  • 404 Not Found - Resource doesn't exist
  • 500 Internal Server Error - Server error

Basic Fetch Requests

GET Request

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

fetchUsers();

// Fetch single user
async function fetchUser(id) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  const user = await response.json();
  return user;
}

fetchUser(1).then(user => console.log(user));

POST Request

JavaScript
// Create new resource
async function createPost(postData) {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(postData)
    });
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const newPost = await response.json();
    console.log('Created post:', newPost);
    return newPost;
  } catch (error) {
    console.error('Failed to create post:', error);
  }
}

// Usage
const postData = {
  title: 'My New Post',
  body: 'This is the content of my post',
  userId: 1
};

createPost(postData);

PUT Request

JavaScript
// Update entire resource
async function updatePost(id, postData) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(postData)
  });
  
  const updatedPost = await response.json();
  return updatedPost;
}

// Usage
updatePost(1, {
  id: 1,
  title: 'Updated Title',
  body: 'Updated content',
  userId: 1
});

PATCH Request

JavaScript
// Partial update
async function patchPost(id, updates) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(updates)
  });
  
  const updatedPost = await response.json();
  return updatedPost;
}

// Usage - only update title
patchPost(1, { title: 'New Title Only' });

DELETE Request

JavaScript
// Delete resource
async function deletePost(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
      method: 'DELETE'
    });
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    console.log(`Post ${id} deleted successfully`);
    return true;
  } catch (error) {
    console.error('Failed to delete post:', error);
    return false;
  }
}

deletePost(1);

Working with Query Parameters

JavaScript
// Build URL with query parameters
function buildURL(baseURL, params) {
  const url = new URL(baseURL);
  Object.keys(params).forEach(key => {
    url.searchParams.append(key, params[key]);
  });
  return url.toString();
}

// Fetch with filters
async function fetchFilteredPosts(filters) {
  const url = buildURL('https://jsonplaceholder.typicode.com/posts', filters);
  console.log('Fetching:', url);
  
  const response = await fetch(url);
  const posts = await response.json();
  return posts;
}

// Usage
fetchFilteredPosts({ userId: 1, _limit: 5 });
// Fetches: https://jsonplaceholder.typicode.com/posts?userId=1&_limit=5

// Alternative using URLSearchParams
async function searchUsers(query) {
  const params = new URLSearchParams({
    q: query,
    _limit: 10
  });
  
  const response = await fetch(`https://jsonplaceholder.typicode.com/users?${params}`);
  const users = await response.json();
  return users;
}

Request Headers and Authentication

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

// Bearer token authentication
async function fetchProtectedData(token) {
  const response = await fetch('https://api.example.com/protected', {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  });
  
  if (response.status === 401) {
    throw new Error('Unauthorized - invalid token');
  }
  
  return await response.json();
}

// API key authentication
async function fetchWithAPIKey(apiKey) {
  const response = await fetch('https://api.example.com/data', {
    headers: {
      'X-API-Key': apiKey
    }
  });
  
  return await response.json();
}

Error Handling

JavaScript
// Comprehensive error handling
async function fetchWithErrorHandling(url) {
  try {
    const response = await fetch(url);
    
    // Check HTTP status
    if (!response.ok) {
      // Try to get error message from response
      let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
      
      try {
        const errorData = await response.json();
        errorMessage = errorData.message || errorMessage;
      } catch (e) {
        // Response is not JSON
      }
      
      throw new Error(errorMessage);
    }
    
    // Parse JSON
    const data = await response.json();
    return { success: true, data };
    
  } catch (error) {
    // Network error or other issues
    if (error.name === 'TypeError') {
      return {
        success: false,
        error: 'Network error - please check your connection'
      };
    }
    
    return {
      success: false,
      error: error.message
    };
  }
}

// Usage
const result = await fetchWithErrorHandling('https://api.example.com/data');
if (result.success) {
  console.log('Data:', result.data);
} else {
  console.error('Error:', result.error);
}

Request Timeout and Abort

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

// Manual abort
const controller = new AbortController();

fetch('https://api.example.com/large-file', {
  signal: controller.signal
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request was cancelled');
    }
  });

// Cancel the request after 2 seconds
setTimeout(() => controller.abort(), 2000);

Practical Example: Complete API Service

JavaScript
// Reusable API service class
class APIService {
  constructor(baseURL, defaultHeaders = {}) {
    this.baseURL = baseURL;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
      ...defaultHeaders
    };
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      ...options,
      headers: {
        ...this.defaultHeaders,
        ...options.headers
      }
    };
    
    try {
      const response = await fetch(url, config);
      
      if (!response.ok) {
        const error = await response.json().catch(() => ({}));
        throw new Error(error.message || `HTTP ${response.status}`);
      }
      
      // Handle empty responses
      const contentType = response.headers.get('content-type');
      if (contentType && contentType.includes('application/json')) {
        return await response.json();
      }
      
      return await response.text();
    } catch (error) {
      console.error(`API Error (${endpoint}):`, error);
      throw error;
    }
  }
  
  async get(endpoint, params = {}) {
    const url = new URL(`${this.baseURL}${endpoint}`);
    Object.keys(params).forEach(key => {
      url.searchParams.append(key, params[key]);
    });
    
    return this.request(url.pathname + url.search);
  }
  
  async post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
  
  async put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }
  
  async patch(endpoint, data) {
    return this.request(endpoint, {
      method: 'PATCH',
      body: JSON.stringify(data)
    });
  }
  
  async delete(endpoint) {
    return this.request(endpoint, {
      method: 'DELETE'
    });
  }
  
  setAuthToken(token) {
    this.defaultHeaders['Authorization'] = `Bearer ${token}`;
  }
}

// Usage
const api = new APIService('https://jsonplaceholder.typicode.com');

// GET request
const users = await api.get('/users');
console.log('Users:', users);

// GET with query params
const filteredPosts = await api.get('/posts', { userId: 1, _limit: 5 });
console.log('Posts:', filteredPosts);

// POST request
const newPost = await api.post('/posts', {
  title: 'New Post',
  body: 'Content here',
  userId: 1
});
console.log('Created:', newPost);

// PUT request
const updated = await api.put('/posts/1', {
  id: 1,
  title: 'Updated Post',
  body: 'Updated content',
  userId: 1
});

// DELETE request
await api.delete('/posts/1');

// With authentication
api.setAuthToken('your-jwt-token-here');
const protectedData = await api.get('/protected/data');

Real-World Example: User Dashboard

JavaScript
// Complete dashboard data fetching
class DashboardService {
  constructor() {
    this.api = new APIService('https://jsonplaceholder.typicode.com');
  }
  
  async loadUserDashboard(userId) {
    try {
      // Fetch all data in parallel
      const [user, posts, todos, albums] = await Promise.all([
        this.api.get(`/users/${userId}`),
        this.api.get('/posts', { userId }),
        this.api.get('/todos', { userId }),
        this.api.get('/albums', { userId })
      ]);
      
      // Calculate statistics
      const stats = {
        totalPosts: posts.length,
        totalTodos: todos.length,
        completedTodos: todos.filter(t => t.completed).length,
        totalAlbums: albums.length
      };
      
      return {
        user,
        posts: posts.slice(0, 5), // Latest 5 posts
        stats,
        recentActivity: this.buildActivity(posts, todos)
      };
    } catch (error) {
      console.error('Failed to load dashboard:', error);
      throw error;
    }
  }
  
  buildActivity(posts, todos) {
    const activity = [
      ...posts.map(p => ({
        type: 'post',
        title: p.title,
        id: p.id
      })),
      ...todos.map(t => ({
        type: 'todo',
        title: t.title,
        completed: t.completed,
        id: t.id
      }))
    ];
    
    return activity.slice(0, 10); // Latest 10 activities
  }
  
  async createPost(userId, postData) {
    return await this.api.post('/posts', {
      ...postData,
      userId
    });
  }
  
  async updateTodo(todoId, completed) {
    return await this.api.patch(`/todos/${todoId}`, {
      completed
    });
  }
}

// Usage
const dashboard = new DashboardService();

async function displayDashboard(userId) {
  try {
    console.log('Loading dashboard...');
    const data = await dashboard.loadUserDashboard(userId);
    
    console.log('User:', data.user.name);
    console.log('Stats:', data.stats);
    console.log('Recent Posts:', data.posts);
    console.log('Recent Activity:', data.recentActivity);
  } catch (error) {
    console.error('Dashboard error:', error);
  }
}

displayDashboard(1);

Key Takeaways

  • Fetch API provides a modern way to make HTTP requests
  • Always check response.ok before parsing response data
  • Use appropriate HTTP methods (GET, POST, PUT, PATCH, DELETE)
  • Handle errors gracefully with try/catch blocks
  • Use URLSearchParams for query parameters
  • Set proper headers for authentication and content type
  • Implement timeout and abort functionality for better UX
  • Create reusable API service classes for maintainability

Practice Exercises

Exercise 1: User Search

Create a function that searches users by name:

Task
// Fetch users from JSONPlaceholder
// Filter by name (case-insensitive)
// Return matching users

async function searchUsers(searchTerm) {
  // Your code here
}

// Test
searchUsers('Leanne').then(users => console.log(users));

Exercise 2: Post Manager

Build a complete CRUD interface for posts:

Task
// Create a PostManager class with methods:
// - getAllPosts()
// - getPost(id)
// - createPost(data)
// - updatePost(id, data)
// - deletePost(id)

class PostManager {
  // Your code here
}

Exercise 3: Retry Failed Requests

Implement automatic retry for failed API calls:

Task
// Create a function that retries failed requests
// with exponential backoff

async function fetchWithRetry(url, maxRetries = 3) {
  // Your code here
  // Hint: Wait 1s, then 2s, then 4s between retries
}

Exercise 4: Parallel Data Loading

Load multiple resources efficiently:

Task
// Fetch user, their posts, and comments in parallel
// Combine into a single object
// Handle partial failures gracefully

async function loadUserProfile(userId) {
  // Your code here
}

What's Next?

In Lesson 06, you'll dive into React, a powerful library for building modern user interfaces. You'll learn about components, state management, hooks, and how to build interactive single-page applications.