Fetch API and HTTP Requests
The Fetch API provides a modern way to make HTTP requests in the browser. It replaces the older XMLHttpRequest with a cleaner, promise-based interface.
Basic GET Request
// Simple fetch
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
With async/await (cleaner):
async function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
getUsers();
Understanding the Response
async function checkResponse() {
const response = await fetch('https://api.example.com/users');
console.log('Status:', response.status); // 200, 404, 500, etc.
console.log('OK:', response.ok); // true if 200-299
console.log('Status text:', response.statusText); // "OK", "Not Found"
console.log('Headers:', response.headers);
console.log('URL:', response.url);
// Parse body
const data = await response.json();
}
Response Parsing Methods
// JSON data
const json = await response.json();
// Plain text
const text = await response.text();
// HTML
const html = await response.text();
// Binary data (Blob)
const blob = await response.blob();
// Form data
const formData = await response.formData();
// Array buffer
const buffer = await response.arrayBuffer();
Error Handling
Fetch only rejects on network errors, not HTTP errors (404, 500):
async function fetchWithErrorHandling(url) {
try {
const response = await fetch(url);
// Check if request succeeded
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// Network error or response parsing error
console.error('Fetch error:', error);
throw error;
}
}
// Usage
try {
const users = await fetchWithErrorHandling('/api/users');
console.log(users);
} catch (error) {
// Handle error (show message to user, etc.)
}
POST Request (Creating Data)
async function createUser(userData) {
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();
}
// Usage
const newUser = await createUser({
name: 'Alice',
email: 'alice@example.com'
});
console.log('Created:', newUser);
PUT Request (Updating Data)
async function updateUser(userId, updates) {
const response = await fetch(`https://api.example.com/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.json();
}
// Usage
await updateUser(123, { name: 'Alice Updated' });
DELETE Request
async function deleteUser(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// Some APIs return empty response
if (response.status === 204) {
return null;
}
return await response.json();
}
Request Headers
async function fetchWithHeaders() {
const response = await fetch('https://api.example.com/data', {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN_HERE',
'X-Custom-Header': 'custom-value'
}
});
return await response.json();
}
Query Parameters
// Manual URL construction
const params = new URLSearchParams({
search: 'nodejs',
page: 2,
limit: 20
});
const url = `https://api.example.com/search?${params}`;
// https://api.example.com/search?search=nodejs&page=2&limit=20
const response = await fetch(url);
const data = await response.json();
Timeouts
Fetch doesn’t have built-in timeout, but you can add one:
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;
}
}
// Usage
try {
const data = await fetchWithTimeout('/api/slow-endpoint', 3000);
} catch (error) {
console.error(error.message); // "Request timeout"
}
Canceling Requests
const controller = new AbortController();
// Start fetch
fetch('https://api.example.com/data', {
signal: controller.signal
})
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('Request canceled');
}
});
// Cancel it
controller.abort();
Useful for search-as-you-type:
let currentController = null;
async function searchAsYouType(query) {
// Cancel previous request
if (currentController) {
currentController.abort();
}
// Create new controller
currentController = new AbortController();
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: currentController.signal
});
const results = await response.json();
displayResults(results);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Search error:', error);
}
}
}
// Each keystroke cancels previous request
searchInput.addEventListener('input', (e) => {
searchAsYouType(e.target.value);
});
Working with FormData
Sending form data (including files):
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'My file');
const response = await fetch('/api/upload', {
method: 'POST',
body: formData // Don't set Content-Type header!
});
return await response.json();
}
// Usage with file input
const fileInput = document.querySelector('#file-input');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (file) {
const result = await uploadFile(file);
console.log('Uploaded:', result);
}
});
CORS (Cross-Origin Requests)
Browsers block cross-origin requests by default:
// This may fail if server doesn't allow CORS
fetch('https://api.other-domain.com/data')
.catch(error => {
// TypeError: Failed to fetch
// Blocked by CORS policy
});
Solutions:
- Server adds CORS headers (backend fix)
- Use a proxy (your server fetches it)
- JSONP (old technique, rarely used now)
Credentials and Cookies
By default, fetch doesn’t send cookies:
// Include cookies in request
fetch('https://api.example.com/user', {
credentials: 'include' // Send cookies
})
// Options:
// 'omit' - never send cookies (default)
// 'same-origin' - send cookies for same origin only
// 'include' - always send cookies
Real-World API Wrapper
class API {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
};
// Add auth token if available
const token = localStorage.getItem('authToken');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
try {
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || `HTTP ${response.status}`);
}
// Handle empty responses
if (response.status === 204) {
return null;
}
return await response.json();
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
get(endpoint) {
return this.request(endpoint);
}
post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
delete(endpoint) {
return this.request(endpoint, {
method: 'DELETE'
});
}
}
// Usage
const api = new API('https://api.example.com');
// GET request
const users = await api.get('/users');
// POST request
const newUser = await api.post('/users', {
name: 'Alice',
email: 'alice@example.com'
});
// PUT request
await api.put('/users/123', { name: 'Alice Updated' });
// DELETE request
await api.delete('/users/123');
Loading States
Show loading indicators:
async function loadUsers() {
const loadingEl = document.querySelector('#loading');
const usersEl = document.querySelector('#users');
const errorEl = document.querySelector('#error');
// Show loading
loadingEl.style.display = 'block';
errorEl.style.display = 'none';
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to load users');
}
const users = await response.json();
// Hide loading, show users
loadingEl.style.display = 'none';
usersEl.innerHTML = users.map(u => `<div>${u.name}</div>`).join('');
} catch (error) {
// Hide loading, show error
loadingEl.style.display = 'none';
errorEl.style.display = 'block';
errorEl.textContent = error.message;
}
}
Best Practices
✅ DO:
- Always handle errors
- Check
response.okbefore parsing - Use async/await for cleaner code
- Set appropriate headers
- Use AbortController for cancellable requests
- Show loading states to users
❌ DON’T:
- Assume fetch succeeded (check response.ok)
- Forget error handling
- Make requests in loops (use Promise.all)
- Expose API keys in client code
- Trust user input without validation
Summary
| Task | Code |
|---|---|
| GET request | fetch(url).then(r => r.json()) |
| POST request | fetch(url, { method: 'POST', body: ... }) |
| With headers | fetch(url, { headers: { ... } }) |
| Error check | if (!response.ok) throw new Error() |
| Parse JSON | await response.json() |
| Timeout | Use AbortController with setTimeout |
| Cancel | controller.abort() |
Next Article: LocalStorage and SessionStorage