Introduction to Fetch Request Configuration
While a simple Fetch request can be made with just a URL, the real power of the Fetch API comes from its configuration options. These options allow you to customize how requests are made, what data is sent, and how the browser should handle various scenarios.
Think of the Fetch API as a sophisticated restaurant ordering system. The URL is like telling the waiter which dish you want, but the configuration options are all the special instructions: how well-done you want your steak, which sides to include, any allergens to avoid, and so on.
Basic Fetch with Configuration
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({ name: 'John', age: 30 })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
The Request Options Object
The second parameter to the fetch() function is an options object that can include numerous properties. Let's explore the most common and useful ones:
HTTP Methods with 'method'
The method property specifies which HTTP method to use for the request. If not specified, it defaults to GET.
Think of HTTP methods as different types of interactions in a library:
- GET: Reading a book without changing it
- POST: Adding a new book to the collection
- PUT: Replacing an existing book with a new edition
- PATCH: Updating specific pages or information about a book
- DELETE: Removing a book from the collection
Different HTTP Methods
// GET request (default)
fetch('https://api.example.com/users');
// POST request to create a resource
fetch('https://api.example.com/users', {
method: 'POST',
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })
});
// PUT request to update/replace a resource
fetch('https://api.example.com/users/123', {
method: 'PUT',
body: JSON.stringify({ name: 'Alice', email: 'newalice@example.com' })
});
// PATCH request to partially update a resource
fetch('https://api.example.com/users/123', {
method: 'PATCH',
body: JSON.stringify({ email: 'newalice@example.com' })
});
// DELETE request to remove a resource
fetch('https://api.example.com/users/123', {
method: 'DELETE'
});
Real-world application: An e-commerce website would use different methods for various operations:
- GET to retrieve product listings and details
- POST to create a new order
- PUT/PATCH to update shipping information
- DELETE to remove items from a cart
Request Headers with 'headers'
The headers property allows you to set HTTP headers for the request. Headers provide additional information about the request or the data being sent.
Think of headers like the metadata on a letter envelope: they provide information about the sender, recipient, handling instructions, and the nature of the contents.
Setting Request Headers
fetch('https://api.example.com/data', {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
'Accept-Language': 'en-US,en;q=0.9',
'X-Requested-With': 'XMLHttpRequest',
'X-Custom-Header': 'CustomValue'
}
});
Common headers and their purposes:
- Content-Type: Specifies the format of the data being sent (e.g., JSON, form data)
- Authorization: Provides authentication credentials
- Accept: Indicates what type of content the client can process in response
- User-Agent: Identifies the client making the request
- Cache-Control: Specifies caching directives
Note: Some headers are "forbidden" and cannot be set manually. These are controlled by the browser and include headers like Host, Connection, Content-Length, etc. The browser will automatically handle these headers.
Using the Headers API
Instead of using a plain object, you can use the Headers API for more control:
const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Authorization', 'Bearer token123');
// Check if a header exists
console.log(headers.has('Content-Type')); // true
// Get a header value
console.log(headers.get('Authorization')); // "Bearer token123"
fetch('https://api.example.com/data', {
headers: headers
});
Real-world application: A multi-user document collaboration app might use headers to specify:
- Which document version the user is editing (via an ETag header)
- The user's permission level (via custom authorization headers)
- The expected format of the response (via Accept headers)
Request Body with 'body'
The body property contains the data to be sent with the request. It's used for methods like POST, PUT, and PATCH.
Think of the body as the contents of a package you're sending. Different types of packages require different preparation methods.
Different Body Types
// JSON data
fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com',
age: 30
})
});
// Form data (automatically sets the appropriate Content-Type)
const formData = new FormData();
formData.append('username', 'johndoe');
formData.append('avatar', fileInput.files[0]);
fetch('https://api.example.com/profile', {
method: 'POST',
body: formData
});
// URL parameters (for application/x-www-form-urlencoded)
const params = new URLSearchParams();
params.append('search', 'query term');
params.append('sort', 'ascending');
fetch('https://api.example.com/search', {
method: 'POST',
body: params
});
// Plain text
fetch('https://api.example.com/logs', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: 'User logged in at 15:32:45'
});
// Binary data
const imageBlob = await fetch('image.jpg').then(r => r.blob());
fetch('https://api.example.com/upload', {
method: 'POST',
body: imageBlob
});
Real-world application: A social media application would use different body types for different features:
- JSON for posting structured content like comments
- FormData for uploading media files with metadata
- URLSearchParams for search queries
- Blob for direct image manipulations
CORS with 'mode'
The mode property controls how the request handles Cross-Origin Resource Sharing (CORS). It affects whether the request can access resources from different origins.
Think of CORS like international travel policies. Different "modes" represent different levels of passport/visa requirements:
- cors (default): Standard cross-origin requests with CORS headers
- no-cors: Limited cross-origin requests (can't access response content)
- same-origin: Fails if the URL is from a different origin
- navigate: Used by navigation in HTML
CORS Modes
// Default mode - requires the server to have appropriate CORS headers
fetch('https://different-domain.com/api/data');
// no-cors mode - very limited response access
fetch('https://different-domain.com/api/data', {
mode: 'no-cors'
});
// same-origin - will fail for cross-origin requests
fetch('https://different-domain.com/api/data', {
mode: 'same-origin'
});
Important: The 'no-cors' mode doesn't mean "bypass CORS restrictions". It actually creates a more restricted request that can't access the response content. It's mainly useful for caching or side-effect operations.
Real-world application: A dashboard that integrates with multiple third-party services:
- Uses 'cors' mode for APIs that support proper CORS
- Uses server-side proxies for APIs that don't support CORS
- Uses 'same-origin' for sensitive internal API calls to prevent CSRF attacks
Authentication with 'credentials'
The credentials property determines whether cookies, HTTP authentication, and client-side SSL certificates are sent with the request.
Think of this like deciding what ID or access cards you bring to different buildings:
- same-origin (default): Send credentials only for same-origin requests
- include: Always send credentials, even for cross-origin requests
- omit: Never send credentials, even for same-origin requests
Credentials Options
// Default - only sends credentials for same-origin requests
fetch('https://api.example.com/profile');
// Always include credentials (cookies, etc.)
fetch('https://api.example.com/profile', {
credentials: 'include'
});
// Never include credentials
fetch('https://api.example.com/public-data', {
credentials: 'omit'
});
Security Note: When using credentials: 'include' for cross-origin requests, the server MUST specify Access-Control-Allow-Credentials: true in its response headers, otherwise the browser will block the response.
Real-world application: A single sign-on (SSO) system might:
- Use 'include' to maintain login state across multiple related subdomains
- Use 'omit' for public API queries that don't need authentication
- Use 'same-origin' for regular authenticated requests within the main application
Cache Control with 'cache'
The cache property determines how the request interacts with the browser's HTTP cache.
Think of this like giving instructions to a personal assistant about whether to check their notes or always get fresh information:
- default: Normal browser caching rules apply
- no-store: Browser will not cache the request or response
- reload: Browser will reload from the server, but will store the response in cache
- no-cache: Browser must verify with the server before using a cached response
- force-cache: Browser will use a cached response if available, regardless of age
- only-if-cached: Browser will only use cached responses and won't go to the network
Cache Options
// Default caching behavior
fetch('https://api.example.com/data');
// Bypass cache completely
fetch('https://api.example.com/realtime-data', {
cache: 'no-store'
});
// Always check for fresh data
fetch('https://api.example.com/updated-data', {
cache: 'no-cache'
});
// Use cached version if available
fetch('https://api.example.com/static-data', {
cache: 'force-cache'
});
// Only use cached data (offline mode)
fetch('https://api.example.com/reference-data', {
cache: 'only-if-cached',
mode: 'same-origin' // This is required for only-if-cached
});
Real-world application: A weather application might use different cache strategies:
- 'no-store' for current weather conditions that change frequently
- 'no-cache' for daily forecasts that should be verified but could be cached
- 'force-cache' for historical weather data that rarely changes
- 'only-if-cached' for offline mode viewing of previously fetched data
Other Important Options
redirect
Controls how the fetch handles redirects:
- follow (default): Automatically follow redirects
- error: Reject the promise on redirects
- manual: Return the redirect response for manual handling
referrer
Controls the referrer header sent with the request:
fetch('https://api.example.com/data', {
referrer: 'https://example.com/page'
});
referrerPolicy
Controls how much referrer information is included:
- no-referrer: Doesn't send any referrer information
- origin: Only sends the origin
- same-origin: Only sends referrer for same-origin requests
- Several other policies for fine-grained control
integrity
Verifies the response matches an expected hash, preventing tampered resources:
fetch('https://cdn.example.com/library.js', {
integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC'
});
keepalive
Allows the request to outlive the page that initiated it:
// Will complete even if the user navigates away
fetch('https://api.example.com/log', {
method: 'POST',
keepalive: true,
body: JSON.stringify({ event: 'page_exit' })
});
signal
Allows you to abort the fetch request:
const controller = new AbortController();
const signal = controller.signal;
// Set a timeout to abort the fetch after 5 seconds
setTimeout(() => {
controller.abort();
}, 5000);
fetch('https://api.example.com/data', {
signal: signal
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Error:', error);
}
});
Practical Examples
File Upload with Progress
const fileInput = document.querySelector('#fileInput');
const progressBar = document.querySelector('#progressBar');
const uploadButton = document.querySelector('#uploadButton');
uploadButton.addEventListener('click', async () => {
if (!fileInput.files.length) return;
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
// Create a controller to potentially abort the upload
const controller = new AbortController();
try {
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
body: formData,
signal: controller.signal
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
const result = await response.json();
console.log('Upload successful:', result);
} catch (error) {
console.error('Upload error:', error);
}
});
// Add a cancel button functionality
cancelButton.addEventListener('click', () => {
controller.abort();
});
Authenticated API Request
async function fetchUserData(userId) {
// Get auth token from storage
const token = localStorage.getItem('authToken');
try {
const response = await fetch(`https://api.example.com/users/${userId}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
credentials: 'include', // Include cookies for session info
mode: 'cors',
cache: 'no-cache' // Always get fresh user data
});
if (response.status === 401) {
// Token expired, redirect to login
window.location.href = '/login';
return;
}
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error fetching user data:', error);
throw error;
}
}
Real-time Data with Cache Control
// Function to fetch stock price with appropriate caching
async function fetchStockPrice(symbol, refreshRate = 'fast') {
let cacheOption;
// Set cache strategy based on refresh rate preference
switch (refreshRate) {
case 'real-time':
cacheOption = 'no-store'; // Never cache
break;
case 'fast':
cacheOption = 'no-cache'; // Validate with server
break;
case 'normal':
cacheOption = 'default'; // Normal browser caching
break;
case 'slow':
cacheOption = 'force-cache'; // Use cache when available
break;
default:
cacheOption = 'default';
}
try {
const response = await fetch(`https://api.example.com/stocks/${symbol}`, {
cache: cacheOption,
headers: {
'X-API-Key': 'your-api-key'
}
});
if (!response.ok) {
throw new Error(`Stock API error: ${response.status}`);
}
const data = await response.json();
return data.price;
} catch (error) {
console.error(`Error fetching ${symbol} price:`, error);
return null;
}
}
Best Practices
-
Be explicit about your intentions: Even if using defaults, explicitly setting options like
methodmakes code more readable. - Set appropriate Content-Type headers: Ensures the server correctly interprets your data.
-
Handle credentials carefully: Only use
credentials: 'include'when necessary for security reasons. - Use appropriate cache strategies: Optimize performance by choosing the right cache option for each type of data.
- Implement timeout mechanisms: Use AbortController to prevent requests from hanging indefinitely.
- Create reusable fetch wrappers: Encapsulate common fetch configurations in utility functions.
Creating a Reusable API Client
// api.js - Reusable API client
class ApiClient {
constructor(baseUrl, options = {}) {
this.baseUrl = baseUrl;
this.defaultOptions = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
mode: options.mode || 'cors',
credentials: options.credentials || 'same-origin',
timeout: options.timeout || 30000 // 30 seconds default timeout
};
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}/${endpoint}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(),
options.timeout || this.defaultOptions.timeout);
try {
const response = await fetch(url, {
...this.defaultOptions,
...options,
headers: {
...this.defaultOptions.headers,
...options.headers
},
signal: controller.signal
});
clearTimeout(timeoutId);
// Handle common response scenarios
if (response.status === 401) {
// Handle authentication errors
this.handleAuthError();
throw new Error('Authentication required');
}
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
// Parse response based on content type
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else {
return await response.text();
}
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${options.timeout || this.defaultOptions.timeout}ms`);
}
throw error;
}
}
// Convenience methods for common HTTP methods
async get(endpoint, options = {}) {
return this.request(endpoint, {
...options,
method: 'GET'
});
}
async post(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
}
async put(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'PUT',
body: JSON.stringify(data)
});
}
async delete(endpoint, options = {}) {
return this.request(endpoint, {
...options,
method: 'DELETE'
});
}
// Handle auth errors (e.g., redirect to login)
handleAuthError() {
console.log('Authentication error, redirecting to login');
// window.location.href = '/login';
}
}
// Usage:
const api = new ApiClient('https://api.example.com', {
headers: {
'X-API-Key': 'your-api-key'
},
credentials: 'include',
timeout: 5000 // 5 second timeout
});
// Now use the client
async function getUserData() {
try {
const user = await api.get('users/me');
console.log(user);
// Post new data
const updated = await api.post('users/me/preferences', {
theme: 'dark',
notifications: true
});
console.log(updated);
} catch (error) {
console.error('API error:', error);
}
}
Practice Exercises
Exercise 1: Authentication Client
Create a fetch-based authentication client that handles:
- User login (POST with credentials)
- Token storage
- Automatic token refresh
- Logout functionality
Exercise 2: File Upload with Progress
Build a file upload system that:
- Accepts multiple files
- Shows upload progress
- Allows cancellation of uploads
- Handles various error scenarios
Exercise 3: API Client Library
Extend the API client example to include:
- Rate limiting handling
- Retry logic for failed requests
- Caching strategies for different endpoints
- Request queuing for offline scenarios
Summary
The Fetch API's configuration options provide powerful control over how HTTP requests are made:
- method determines the HTTP method (GET, POST, etc.)
- headers sets HTTP headers for the request
- body contains the data to send with the request
- mode controls CORS behavior
- credentials manages cookies and authentication info
- cache determines caching behavior
- Other options like redirect, referrer, integrity, and signal provide additional control
By mastering these configuration options, you can build sophisticated networking capabilities into your web applications, handling everything from complex authentication scenarios to optimized data loading patterns.