How to fetch data from an API with fetch()
How to Fetch Data from an API with fetch()
Table of Contents
1. [Introduction](#introduction)
2. [Prerequisites](#prerequisites)
3. [Understanding the fetch() API](#understanding-the-fetch-api)
4. [Basic Syntax and Structure](#basic-syntax-and-structure)
5. [Making Your First API Request](#making-your-first-api-request)
6. [Handling Different HTTP Methods](#handling-different-http-methods)
7. [Working with Request Headers](#working-with-request-headers)
8. [Error Handling and Response Management](#error-handling-and-response-management)
9. [Advanced fetch() Techniques](#advanced-fetch-techniques)
10. [Real-World Examples](#real-world-examples)
11. [Common Issues and Troubleshooting](#common-issues-and-troubleshooting)
12. [Best Practices and Professional Tips](#best-practices-and-professional-tips)
13. [Performance Optimization](#performance-optimization)
14. [Conclusion](#conclusion)
Introduction
The `fetch()` API is a modern, powerful, and flexible method for making HTTP requests in JavaScript. It provides a cleaner, more intuitive alternative to the older `XMLHttpRequest` object and has become the standard way to interact with APIs in web development. This comprehensive guide will teach you everything you need to know about using `fetch()` to retrieve data from APIs, handle responses, manage errors, and implement best practices for robust web applications.
Whether you're building a simple weather app, integrating with social media APIs, or developing complex enterprise applications, mastering the `fetch()` API is essential for modern JavaScript development. By the end of this article, you'll have a thorough understanding of how to effectively use `fetch()` in various scenarios and handle common challenges that arise when working with external APIs.
Prerequisites
Before diving into the `fetch()` API, ensure you have:
- Basic JavaScript Knowledge: Understanding of variables, functions, objects, and arrays
- Promise Fundamentals: Familiarity with JavaScript Promises and asynchronous programming
- ES6+ Syntax: Knowledge of arrow functions, destructuring, and template literals
- HTTP Basics: Understanding of HTTP methods (GET, POST, PUT, DELETE) and status codes
- JSON Format: Ability to work with JSON data structures
- Development Environment: A code editor and browser with developer tools
- Basic HTML/CSS: For creating simple test pages to demonstrate concepts
Understanding the fetch() API
The `fetch()` API is a Web API that provides a JavaScript interface for accessing and manipulating parts of the HTTP pipeline, such as requests and responses. It's built on top of Promises, making it naturally asynchronous and perfect for modern JavaScript development patterns.
Key Features of fetch()
- Promise-based: Returns Promises for cleaner asynchronous code
- Flexible: Supports various request methods and configurations
- Streaming: Can handle response bodies as streams
- CORS-aware: Built-in support for Cross-Origin Resource Sharing
- Modern: Designed for contemporary web development practices
Browser Support
The `fetch()` API is supported in all modern browsers:
- Chrome 42+
- Firefox 39+
- Safari 10.1+
- Edge 14+
For older browsers, you can use polyfills like `whatwg-fetch`.
Basic Syntax and Structure
The basic syntax of `fetch()` is straightforward:
```javascript
fetch(url, options)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
```
Parameters
1. url (required): The URL of the resource you want to fetch
2. options (optional): An object containing request configuration
Return Value
`fetch()` returns a Promise that resolves to a Response object representing the response to the request.
Making Your First API Request
Let's start with a simple GET request to fetch data from a public API:
```javascript
// Basic GET request
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Success:', data);
// Handle the data here
document.getElementById('title').textContent = data.title;
document.getElementById('body').textContent = data.body;
})
.catch(error => {
console.error('Error:', error);
});
```
Using async/await Syntax
For cleaner, more readable code, you can use async/await:
```javascript
async function fetchPost() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Success:', data);
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// Usage
fetchPost().then(data => {
// Handle successful response
}).catch(error => {
// Handle error
});
```
Handling Different HTTP Methods
GET Requests
GET requests are the default method for `fetch()`:
```javascript
// Explicit GET request
const getData = async () => {
try {
const response = await fetch('https://api.example.com/data', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
return await response.json();
} catch (error) {
console.error('GET request failed:', error);
}
};
```
POST Requests
POST requests are used to send data to the server:
```javascript
const postData = async (userData) => {
try {
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();
} catch (error) {
console.error('POST request failed:', error);
throw error;
}
};
// Usage
const newUser = {
name: 'John Doe',
email: 'john@example.com',
age: 30
};
postData(newUser)
.then(result => console.log('User created:', result))
.catch(error => console.error('Failed to create user:', error));
```
PUT Requests
PUT requests are used to update existing resources:
```javascript
const updateData = async (id, updatedData) => {
try {
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('PUT request failed:', error);
throw error;
}
};
```
DELETE Requests
DELETE requests are used to remove resources:
```javascript
const deleteData = async (id) => {
try {
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.status === 204 ? null : await response.json();
} catch (error) {
console.error('DELETE request failed:', error);
throw error;
}
};
```
Working with Request Headers
Headers provide additional information about the request or response. Here's how to work with them effectively:
Setting Request Headers
```javascript
const fetchWithHeaders = async () => {
try {
const response = await fetch('https://api.example.com/protected-data', {
method: 'GET',
headers: {
'Authorization': 'Bearer your-token-here',
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'MyApp/1.0',
'X-Custom-Header': 'custom-value'
}
});
return await response.json();
} catch (error) {
console.error('Request with headers failed:', error);
}
};
```
Dynamic Headers
```javascript
const createAuthenticatedRequest = (token) => {
const headers = new Headers();
headers.append('Authorization', `Bearer ${token}`);
headers.append('Content-Type', 'application/json');
return {
method: 'GET',
headers: headers
};
};
// Usage
const token = 'your-jwt-token';
const options = createAuthenticatedRequest(token);
fetch('https://api.example.com/user-profile', options)
.then(response => response.json())
.then(data => console.log(data));
```
Reading Response Headers
```javascript
const checkResponseHeaders = async () => {
try {
const response = await fetch('https://api.example.com/data');
// Check specific headers
console.log('Content-Type:', response.headers.get('Content-Type'));
console.log('Cache-Control:', response.headers.get('Cache-Control'));
// Iterate through all headers
for (let [key, value] of response.headers) {
console.log(`${key}: ${value}`);
}
return await response.json();
} catch (error) {
console.error('Error reading headers:', error);
}
};
```
Error Handling and Response Management
Proper error handling is crucial for robust applications. The `fetch()` API has some unique characteristics when it comes to error handling.
Understanding fetch() Error Behavior
Important: `fetch()` only rejects the Promise for network errors, not HTTP error status codes (like 404 or 500).
```javascript
const robustFetch = async (url, options = {}) => {
try {
const response = await fetch(url, options);
// Check if the response is ok (status in the range 200-299)
if (!response.ok) {
// Handle different HTTP status codes
switch (response.status) {
case 400:
throw new Error('Bad Request - Check your request parameters');
case 401:
throw new Error('Unauthorized - Check your authentication');
case 403:
throw new Error('Forbidden - You do not have permission');
case 404:
throw new Error('Not Found - The requested resource does not exist');
case 500:
throw new Error('Internal Server Error - Please try again later');
default:
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
}
}
return response;
} catch (error) {
// Handle network errors and our custom errors
if (error instanceof TypeError) {
throw new Error('Network error - Check your internet connection');
}
throw error;
}
};
```
Comprehensive Error Handling Example
```javascript
const fetchDataWithErrorHandling = async (url) => {
try {
const response = await robustFetch(url);
// Check content type before parsing
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error('Response is not JSON');
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch operation failed:', error.message);
// You might want to show user-friendly messages
const userMessage = getUserFriendlyMessage(error.message);
showErrorToUser(userMessage);
throw error; // Re-throw if you want calling code to handle it
}
};
const getUserFriendlyMessage = (errorMessage) => {
if (errorMessage.includes('Network error')) {
return 'Please check your internet connection and try again.';
} else if (errorMessage.includes('Unauthorized')) {
return 'Please log in to access this content.';
} else if (errorMessage.includes('Not Found')) {
return 'The requested information could not be found.';
}
return 'Something went wrong. Please try again later.';
};
const showErrorToUser = (message) => {
// Implementation depends on your UI framework
console.error('User Error:', message);
// Example: document.getElementById('error-message').textContent = message;
};
```
Advanced fetch() Techniques
Request Timeout
`fetch()` doesn't have a built-in timeout option, but you can implement one using `AbortController`:
```javascript
const fetchWithTimeout = async (url, options = {}, timeout = 5000) => {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(id);
return response;
} catch (error) {
clearTimeout(id);
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeout}ms`);
}
throw error;
}
};
// Usage
fetchWithTimeout('https://api.example.com/slow-endpoint', {}, 3000)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Request failed:', error.message));
```
Request Cancellation
```javascript
class APIClient {
constructor() {
this.activeRequests = new Map();
}
async fetch(key, url, options = {}) {
// Cancel previous request with the same key
if (this.activeRequests.has(key)) {
this.activeRequests.get(key).abort();
}
const controller = new AbortController();
this.activeRequests.set(key, controller);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
this.activeRequests.delete(key);
return response;
} catch (error) {
this.activeRequests.delete(key);
throw error;
}
}
cancelRequest(key) {
if (this.activeRequests.has(key)) {
this.activeRequests.get(key).abort();
this.activeRequests.delete(key);
}
}
cancelAllRequests() {
for (let [key, controller] of this.activeRequests) {
controller.abort();
}
this.activeRequests.clear();
}
}
// Usage
const apiClient = new APIClient();
// Start a request
apiClient.fetch('user-data', 'https://api.example.com/user')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name !== 'AbortError') {
console.error('Request failed:', error);
}
});
// Cancel the request if needed
// apiClient.cancelRequest('user-data');
```
Retry Logic
```javascript
const fetchWithRetry = async (url, options = {}, maxRetries = 3, delay = 1000) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return response;
}
// Don't retry for client errors (4xx)
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}
// Retry for server errors (5xx) and other issues
if (attempt === maxRetries) {
throw new Error(`Server error after ${maxRetries} attempts: ${response.status}`);
}
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2;
}
}
};
```
Real-World Examples
Weather App Integration
```javascript
class WeatherService {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.openweathermap.org/data/2.5';
}
async getCurrentWeather(city) {
const url = `${this.baseUrl}/weather?q=${encodeURIComponent(city)}&appid=${this.apiKey}&units=metric`;
try {
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) {
throw new Error('City not found');
}
throw new Error(`Weather API error: ${response.status}`);
}
const data = await response.json();
return {
city: data.name,
country: data.sys.country,
temperature: data.main.temp,
description: data.weather[0].description,
humidity: data.main.humidity,
windSpeed: data.wind.speed
};
} catch (error) {
console.error('Failed to fetch weather data:', error);
throw error;
}
}
async getForecast(city, days = 5) {
const url = `${this.baseUrl}/forecast?q=${encodeURIComponent(city)}&appid=${this.apiKey}&units=metric&cnt=${days * 8}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Forecast API error: ${response.status}`);
}
const data = await response.json();
return data.list.map(item => ({
date: new Date(item.dt * 1000),
temperature: item.main.temp,
description: item.weather[0].description
}));
} catch (error) {
console.error('Failed to fetch forecast data:', error);
throw error;
}
}
}
// Usage
const weatherService = new WeatherService('your-api-key');
async function displayWeather() {
try {
const weather = await weatherService.getCurrentWeather('London');
console.log(`Current weather in ${weather.city}: ${weather.temperature}°C, ${weather.description}`);
} catch (error) {
console.error('Error displaying weather:', error.message);
}
}
```
REST API Client
```javascript
class RESTClient {
constructor(baseURL, defaultHeaders = {}) {
this.baseURL = baseURL.replace(/\/$/, ''); // Remove trailing slash
this.defaultHeaders = {
'Content-Type': 'application/json',
...defaultHeaders
};
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}/${endpoint.replace(/^\//, '')}`;
const config = {
headers: { ...this.defaultHeaders, ...options.headers },
...options
};
if (config.body && typeof config.body === 'object') {
config.body = JSON.stringify(config.body);
}
try {
const response = await fetch(url, config);
if (!response.ok) {
const errorData = await response.text();
throw new Error(`HTTP ${response.status}: ${errorData}`);
}
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(`Request failed: ${config.method || 'GET'} ${url}`, error);
throw error;
}
}
get(endpoint, headers = {}) {
return this.request(endpoint, { method: 'GET', headers });
}
post(endpoint, body, headers = {}) {
return this.request(endpoint, { method: 'POST', body, headers });
}
put(endpoint, body, headers = {}) {
return this.request(endpoint, { method: 'PUT', body, headers });
}
delete(endpoint, headers = {}) {
return this.request(endpoint, { method: 'DELETE', headers });
}
}
// Usage
const api = new RESTClient('https://jsonplaceholder.typicode.com');
// GET request
api.get('posts/1')
.then(post => console.log('Post:', post))
.catch(error => console.error('Error:', error));
// POST request
api.post('posts', {
title: 'New Post',
body: 'This is the content of the new post',
userId: 1
})
.then(result => console.log('Created:', result))
.catch(error => console.error('Error:', error));
```
Common Issues and Troubleshooting
CORS Issues
Cross-Origin Resource Sharing (CORS) is a common issue when working with APIs:
```javascript
// This might fail due to CORS policy
fetch('https://api.external-service.com/data')
.then(response => response.json())
.catch(error => {
if (error.message.includes('CORS')) {
console.error('CORS error - the API does not allow requests from this domain');
// Consider using a proxy server or CORS proxy service
}
});
// Solution 1: Use a CORS proxy (for development only)
const proxyUrl = 'https://cors-anywhere.herokuapp.com/';
const targetUrl = 'https://api.external-service.com/data';
fetch(proxyUrl + targetUrl)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// Solution 2: Configure your server to handle CORS
// This is the preferred production solution
```
JSON Parsing Errors
```javascript
const safeJsonParse = async (response) => {
const text = await response.text();
if (!text) {
return null;
}
try {
return JSON.parse(text);
} catch (error) {
console.error('Invalid JSON response:', text);
throw new Error('Response is not valid JSON');
}
};
// Usage
fetch('https://api.example.com/data')
.then(safeJsonParse)
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
```
Network Connectivity Issues
```javascript
const checkNetworkAndFetch = async (url, options = {}) => {
// Check if online
if (!navigator.onLine) {
throw new Error('No internet connection');
}
try {
const response = await fetch(url, options);
return response;
} catch (error) {
if (error instanceof TypeError) {
// Network error
throw new Error('Network error - please check your connection');
}
throw error;
}
};
// Listen for online/offline events
window.addEventListener('online', () => {
console.log('Connection restored');
// Retry failed requests
});
window.addEventListener('offline', () => {
console.log('Connection lost');
// Handle offline state
});
```
Memory Leaks with Large Responses
```javascript
const fetchLargeData = async (url) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// For large responses, consider streaming
const reader = response.body.getReader();
const chunks = [];
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
// Process chunks as needed
const fullData = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));
let offset = 0;
for (const chunk of chunks) {
fullData.set(chunk, offset);
offset += chunk.length;
}
return fullData;
} finally {
reader.releaseLock();
}
};
```
Best Practices and Professional Tips
1. Create Reusable API Functions
```javascript
// Create a configuration object for your API
const API_CONFIG = {
baseURL: process.env.API_BASE_URL || 'https://api.example.com',
timeout: 10000,
retries: 3
};
// Create utility functions
const apiUtils = {
buildUrl: (endpoint, params = {}) => {
const url = new URL(endpoint, API_CONFIG.baseURL);
Object.keys(params).forEach(key => {
if (params[key] !== null && params[key] !== undefined) {
url.searchParams.append(key, params[key]);
}
});
return url.toString();
},
getAuthHeaders: () => {
const token = localStorage.getItem('authToken');
return token ? { 'Authorization': `Bearer ${token}` } : {};
}
};
```
2. Implement Request/Response Interceptors
```javascript
class APIInterceptor {
constructor() {
this.requestInterceptors = [];
this.responseInterceptors = [];
}
addRequestInterceptor(interceptor) {
this.requestInterceptors.push(interceptor);
}
addResponseInterceptor(interceptor) {
this.responseInterceptors.push(interceptor);
}
async fetch(url, options = {}) {
// Apply request interceptors
let finalOptions = options;
for (const interceptor of this.requestInterceptors) {
finalOptions = await interceptor(url, finalOptions);
}
let response = await fetch(url, finalOptions);
// Apply response interceptors
for (const interceptor of this.responseInterceptors) {
response = await interceptor(response);
}
return response;
}
}
// Usage
const api = new APIInterceptor();
// Add logging interceptor
api.addRequestInterceptor(async (url, options) => {
console.log(`Making request to: ${url}`);
return options;
});
// Add auth token interceptor
api.addRequestInterceptor(async (url, options) => {
const token = localStorage.getItem('authToken');
if (token) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${token}`
};
}
return options;
});
```
3. Use Environment-Specific Configuration
```javascript
const getApiConfig = () => {
const env = process.env.NODE_ENV || 'development';
const configs = {
development: {
baseURL: 'http://localhost:3000/api',
timeout: 5000,
debug: true
},
staging: {
baseURL: 'https://staging-api.example.com',
timeout: 8000,
debug: false
},
production: {
baseURL: 'https://api.example.com',
timeout: 10000,
debug: false
}
};
return configs[env] || configs.development;
};
const config = getApiConfig();
```
4. Implement Caching Strategies
```javascript
class CachedAPIClient {
constructor(cacheTimeout = 5 60 1000) { // 5 minutes default
this.cache = new Map();
this.cacheTimeout = cacheTimeout;
}
generateCacheKey(url, options) {
return `${url}_${JSON.stringify(options)}`;
}
async fetch(url, options = {}) {
const cacheKey = this.generateCacheKey(url, options);
const cached = this.cache.get(cacheKey);
// Check if cached data is still valid
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
console.log('Returning cached data for:', url);
return cached.data;
}
try {
const response = await fetch(url, options);
const data = await response.json();
// Cache the response
this.cache.set(cacheKey, {
data,
timestamp: Date.now()
});
return data;
} catch (error) {
// If request fails, return cached data if available
if (cached) {
console.log('Request failed, returning stale cached data');
return cached.data;
}
throw error;
}
}
clearCache() {
this.cache.clear();
}
}
```
5. Handle Rate Limiting
```javascript
class RateLimitedClient {
constructor(requestsPerSecond = 10) {
this.requestsPerSecond = requestsPerSecond;
this.requestQueue = [];
this.isProcessing = false;
}
async fetch(url, options = {}) {
return new Promise((resolve, reject) => {
this.requestQueue.push({ url, options, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.isProcessing || this.requestQueue.length === 0) {
return;
}
this.isProcessing = true;
while (this.requestQueue.length > 0) {
const { url, options, resolve, reject } = this.requestQueue.shift();
try {
const response = await fetch(url, options);
resolve(response);
} catch (error) {
reject(error);
}
// Wait before next request
await new Promise(resolve =>
setTimeout(resolve, 1000 / this.requestsPerSecond)
);
}
this.isProcessing = false;
}
}
```
Performance Optimization
1. Use Request Deduplication
```javascript
class DeduplicatedClient {
constructor() {
this.pendingRequests = new Map();
}
async fetch(url, options = {}) {
const key = `${url}_${JSON.stringify(options)}`;
// If request is already pending, return the same Promise
if (this.pendingRequests.has(key)) {
console.log('Deduplicating request for:', url);
return this.pendingRequests.get(key);
}
// Create new request and store it
const promise = fetch(url, options)
.then(response => {
this.pendingRequests.delete(key);
return response;
})
.catch(error => {
this.pendingRequests.delete(key);
throw error;
});
this.pendingRequests.set(key, promise);
return promise;
}
}
const client = new DeduplicatedClient();
// These will result in only one actual HTTP request
Promise.all([
client.fetch('https://api.example.com/data'),
client.fetch('https://api.example.com/data'),
client.fetch('https://api.example.com/data')
]).then(responses => {
console.log('All responses received');
});
```
2. Implement Connection Pooling
```javascript
class ConnectionPool {
constructor(maxConnections = 6) {
this.maxConnections = maxConnections;
this.activeConnections = 0;
this.queue = [];
}
async fetch(url, options = {}) {
return new Promise((resolve, reject) => {
this.queue.push({ url, options, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.activeConnections >= this.maxConnections || this.queue.length === 0) {
return;
}
const { url, options, resolve, reject } = this.queue.shift();
this.activeConnections++;
try {
const response = await fetch(url, options);
resolve(response);
} catch (error) {
reject(error);
} finally {
this.activeConnections--;
this.processQueue(); // Process next request in queue
}
}
}
```
3. Optimize for Mobile and Slow Networks
```javascript
class NetworkAwareClient {
constructor() {
this.connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
this.isSlowNetwork = this.checkSlowNetwork();
}
checkSlowNetwork() {
if (!this.connection) return false;
const slowTypes = ['slow-2g', '2g'];
return slowTypes.includes(this.connection.effectiveType) ||
this.connection.downlink < 1.5;
}
async fetch(url, options = {}) {
const optimizedOptions = { ...options };
if (this.isSlowNetwork) {
// Reduce timeout for slow networks
optimizedOptions.timeout = 3000;
// Add compression headers
optimizedOptions.headers = {
...optimizedOptions.headers,
'Accept-Encoding': 'gzip, deflate, br'
};
}
return fetch(url, optimizedOptions);
}
// Monitor network changes
startNetworkMonitoring() {
if (this.connection) {
this.connection.addEventListener('change', () => {
this.isSlowNetwork = this.checkSlowNetwork();
console.log('Network condition changed:', this.connection.effectiveType);
});
}
}
}
```
4. Use Streaming for Large Responses
```javascript
async function streamResponse(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let result = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Process chunk immediately
const chunk = decoder.decode(value, { stream: true });
result += chunk;
// You can process data as it arrives
console.log('Received chunk:', chunk.length, 'bytes');
// Optional: Update UI progressively
// updateUI(chunk);
}
return result;
} finally {
reader.releaseLock();
}
}
```
5. Implement Smart Retry with Circuit Breaker
```javascript
class CircuitBreaker {
constructor(failureThreshold = 5, timeout = 60000) {
this.failureThreshold = failureThreshold;
this.timeout = timeout;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async fetch(url, options = {}) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime < this.timeout) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const response = await fetch(url, options);
if (response.ok) {
this.reset();
return response;
}
throw new Error(`HTTP error! status: ${response.status}`);
} catch (error) {
this.recordFailure();
throw error;
}
}
recordFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
console.log('Circuit breaker opened due to failures');
}
}
reset() {
this.failureCount = 0;
this.state = 'CLOSED';
this.lastFailureTime = null;
}
}
```
Conclusion
The `fetch()` API has revolutionized how we handle HTTP requests in JavaScript, providing a modern, promise-based approach that's both powerful and flexible. Throughout this comprehensive guide, we've explored everything from basic GET requests to advanced patterns like circuit breakers and connection pooling.
Key Takeaways
1. Always Handle Errors Properly: Remember that `fetch()` only rejects for network errors, not HTTP status codes. Always check `response.ok` and implement comprehensive error handling.
2. Use Modern Async/Await Syntax: While `.then()` chains work, async/await provides cleaner, more readable code that's easier to debug and maintain.
3. Implement Retry Logic: Network requests can fail for various reasons. Implementing retry logic with exponential backoff makes your applications more resilient.
4. Consider Performance: Use techniques like request deduplication, caching, and streaming for better performance, especially on mobile devices and slow networks.
5. Security First: Always validate and sanitize data, use HTTPS, and implement proper authentication headers when working with sensitive APIs.
6. Plan for Scale: As your application grows, consider implementing patterns like circuit breakers, rate limiting, and connection pooling to handle increased load gracefully.
Moving Forward
The `fetch()` API continues to evolve, with new features and improvements being added regularly. Stay updated with the latest specifications and browser support to take advantage of new capabilities as they become available.
Consider integrating `fetch()` with modern frameworks and libraries like React, Vue, or Angular, and explore how it works with state management solutions and testing frameworks to build robust, maintainable web applications.
Remember that mastering the `fetch()` API is not just about knowing the syntax—it's about understanding HTTP fundamentals, handling edge cases gracefully, and building applications that provide excellent user experiences even when network conditions are challenging.
By following the patterns and best practices outlined in this guide, you'll be well-equipped to handle any API integration challenge and build applications that are reliable, performant, and user-friendly.