The Fetch API – request options, headers, sending JSON and form data, reading responses, and CORS at the level you need when wiring a real front end to an API.
Request shape
const res = await fetch('/api/items', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ name: 'Widget', qty: 2 }),
credentials: 'same-origin',
});
credentials controls cookies: omit, same-origin (default), or include for cross-origin cookies when the server allows it.
Reading the response
const type = res.headers.get('Content-Type') ?? '';
let data;
if (type.includes('application/json')) {
data = await res.json();
} else {
data = await res.text();
}
You can only read the body once. Clone with res.clone() if you need both text and json (rare).
FormData and uploads
const form = document.querySelector('#upload-form');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const body = new FormData(form);
const res = await fetch('/api/upload', { method: 'POST', body });
if (!res.ok) throw new Error('Upload failed');
});
Do not set Content-Type manually for FormData – the browser adds the multipart boundary.
URLSearchParams for query strings
const params = new URLSearchParams({
q: searchTerm,
page: String(page),
});
const res = await fetch(`/api/search?${params}`);
CORS in one paragraph
Browsers block JavaScript from reading many cross-origin responses unless the server sends permissive CORS headers. You will see opaque network errors or CORS messages in the console. Fixing it is a server job (Access-Control-Allow-Origin, methods, headers). From the client, use the correct URL, avoid unnecessary custom headers on simple GETs, and during development consider a local proxy if you control the front end but not the API yet.
A small API helper
async function api(path, { method = 'GET', body, headers = {} } = {}) {
const init = {
method,
headers: { Accept: 'application/json', ...headers },
};
if (body !== undefined) {
init.headers['Content-Type'] = 'application/json';
init.body = JSON.stringify(body);
}
const res = await fetch(path, init);
if (!res.ok) {
const message = await res.text().catch(() => '');
throw new Error(`${method} ${path} failed: ${res.status} ${message}`);
}
if (res.status === 204) return null;
return res.json();
}
Centralising fetch logic saves repeating status checks and JSON parsing in every caller.
Next: DOM performance – batching updates, observers, and avoiding layout thrash.

