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 serverPOST- Create new data on the serverPUT- 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 succeeded201 Created- Resource created successfully400 Bad Request- Invalid request data401 Unauthorized- Authentication required404 Not Found- Resource doesn't exist500 Internal Server Error- Server error
Basic Fetch Requests
GET Request
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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:
// 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:
// 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:
// 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:
// Fetch user, their posts, and comments in parallel
// Combine into a single object
// Handle partial failures gracefully
async function loadUserProfile(userId) {
// Your code here
}
Additional Resources
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.