Fetch API
Beyond simple fetch(), the Fetch API provides Request, Response, and Headers objects for fine-grained control over HTTP interactions.
The Headers Object
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.append('X-Custom', 'value1');
headers.append('X-Custom', 'value2');
headers.get('Content-Type'); // "application/json"
headers.get('X-Custom'); // "value1" (first only)
headers.getAll('X-Custom'); // ["value1", "value2"]
headers.has('Content-Type'); // true
headers.delete('X-Custom');
// From an object
const headers = new Headers({
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
});
Demo: Headers Object
HTML
<div>
<button id='headers-btn'>Inspect Response Headers</button>
<pre id='headers-out' style='background:#f1f5f9;padding:12px;border-radius:6px;'></pre>
</div> JavaScript
const out = document.getElementById('headers-out');
document.getElementById('headers-btn').onclick = async function() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
let info = 'Response Headers:\\n';
for (const [key, value] of response.headers) {
info += ' ' + key + ': ' + value + '\\n';
}
info += '---\\n';
info += 'content-type: ' + response.headers.get('content-type') + '\\n';
info += 'Has x-ratelimit: ' + response.headers.has('x-ratelimit-remaining');
out.textContent = info;
}; Live Output Window
The Request Object
Create reusable request objects:
const request = new Request('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice' }),
cache: 'no-cache',
credentials: 'same-origin'
});
// Clone before reusing (Request/Response can only be used once)
const requestCopy = request.clone();
fetch(request);
Demo: Request Object
HTML
<div>
<button id='request-btn'>Send with Request Object</button>
<pre id='request-out' style='background:#f1f5f9;padding:12px;border-radius:6px;'></pre>
</div> JavaScript
const out = document.getElementById('request-out');
document.getElementById('request-btn').onclick = async function() {
const request = new Request('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Debug': 'fetch-api-demo'
},
body: JSON.stringify({
title: 'Using Request Object',
body: 'This was sent using a Request object',
userId: 1
})
});
out.textContent =
'Request URL: ' + request.url + '\\n' +
'Request Method: ' + request.method + '\\n' +
'Content-Type: ' + request.headers.get('Content-Type') + '\\n' +
'---\\n';
const response = await fetch(request);
const data = await response.json();
out.textContent += 'Response:' + '\\n' + JSON.stringify(data, null, 2);
}; Live Output Window
The Response Object
fetch() returns a Response object. You can also create your own:
const response = new Response('{"ok":true}', {
status: 200,
statusText: 'OK',
headers: { 'Content-Type': 'application/json' }
});
response.json(); // { ok: true }
Request and Response are One-Time Use
Important: Request and Response bodies can only be read once:
const response = await fetch(url);
const text = await response.text(); // β
works
const json = await response.json(); // β TypeError: Already read
// Solution: clone before reading
const clone = response.clone();
const text = await response.text();
const json = await clone.json();
Creating a Custom Response
Useful for service workers:
// In a service worker
self.addEventListener('fetch', function(event) {
const body = JSON.stringify({ online: true });
const response = new Response(body, {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
event.respondWith(response);
});
Fetch Interceptors (Pattern)
While fetch doesnβt have built-in interceptors, you can wrap it:
async function myFetch(url, options = {}) {
// Request interceptor
const headers = {
'Authorization': `Bearer ${getToken()}`,
'X-Request-ID': crypto.randomUUID(),
...options.headers
};
const start = performance.now();
try {
const response = await fetch(url, { ...options, headers });
// Response interceptor
const duration = performance.now() - start;
console.log(`${url} β ${response.status} (${duration.toFixed(0)}ms)`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
} catch (err) {
// Error interceptor
console.error(`Request failed: ${url}`, err);
throw err;
}
}
Headers Comparison
| Operation | new Headers() | Plain Object |
|---|---|---|
| Set header | .set('key', 'val') | { key: 'val' } |
| Append multi-value | .append('k', 'v2') | Canβt |
| Iterate | for...of | Object.entries() |
| Case-insensitive | Yes | No |
Key Takeaways
Headersis a case-insensitive multi-map for HTTP headersRequestobjects bundle URL + options for reuseResponseobjects can be constructed manually (useful in service workers)- Bodies can only be read once β clone if you need multiple reads
- Wrap fetch in a custom function for interceptor patterns
- Both
RequestandResponseimplement theBodyinterface (.json(),.text(),.blob())