APIs: Concepts, Design, Requests, Responses, and Integration
Using the Fetch API for HTTP Requests
Learn how to use the browser Fetch API with async/await, JSON, forms, headers, errors, cancellation, CORS, authentication, caching, and streams.
The Fetch API is a browser interface for making HTTP requests and receiving responses through Promises. It is used to retrieve API data, submit forms, update application state, and load resources such as JSON, text, images, and files.
This lesson assumes familiarity with JavaScript functions, objects, arrays, Promises, async/await, try/catch, URLs, HTTP methods, headers, status codes, and JSON.
What Fetch Does
Calling fetch() starts a network request. It returns a Promise, which represents asynchronous work that will eventually complete or fail. The fulfilled value is a Response object, not the parsed API data itself.
Fetch is conceptually simpler than the older XMLHttpRequest API. XMLHttpRequest uses event handlers and mutable request state, while Fetch uses Promises and works naturally with async/await. Both can make HTTP requests; Fetch does not automatically treat HTTP error statuses as rejected Promises.
The Basic Request Flow
- Start a request with
fetch(url). - Receive a
Responseobject when the network exchange completes. - Check the HTTP status and response metadata.
- Read and parse the body with a method such as
json()ortext(). - Use the resulting value to update application state or the interface.
async function loadItems() {
try {
const response = await fetch('/api/items');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const items = await response.json();
console.log(items);
} catch (error) {
console.error('Could not load items:', error);
}
}
The same flow can be written with Promise chaining:
fetch('/api/items')
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(items => console.log(items))
.catch(error => console.error('Request failed:', error));
Understanding the Response Object
A Response represents the received HTTP response. Useful properties include:
ok:truefor status codes from 200 through 299.status: the numeric HTTP status, such as200,404, or500.statusText: the associated reason phrase when supplied by the environment.headers: aHeadersobject containing response metadata.url: the final response URL.redirected: whether the request followed a redirect.type: the response type, such asbasic,cors,opaque, orerror.
const response = await fetch('/api/profile');
console.log(response.status, response.statusText);
console.log(response.url, response.redirected, response.type);
console.log(response.headers.get('Content-Type'));
if (response.status !== 200) {
throw new Error('The profile was not returned');
}
A response body is normally a one-use stream. After calling response.json(), the body is consumed and cannot normally be read again with response.text(). Use response.clone() before consumption when two independent consumers need the body.
Parsing Response Bodies
Every body-reading method returns a Promise. Choose the method that matches the expected payload and its Content-Type.
| Method | Result type | Typical content type | Common use |
|---|---|---|---|
json() | JavaScript value | application/json | API data |
text() | String | text/plain or HTML | Messages, markup, source text |
blob() | Blob | Images, PDFs, downloads | Displaying or downloading a file |
arrayBuffer() | ArrayBuffer | Binary data | Custom binary processing |
formData() | FormData | Form-encoded response | Reading form-style payloads |
const json = await response.json();
const message = await response.text();
const imageBlob = await response.blob();
const bytes = await response.arrayBuffer();
const fields = await response.formData();
Request Configuration with RequestInit
The optional second argument to fetch() is a RequestInit options object. The default method is GET.
| Option | Purpose | Typical values | Important notes |
|---|---|---|---|
method | Chooses the HTTP action | GET, POST, PUT, PATCH, DELETE | GET and HEAD should not have bodies |
headers | Supplies request metadata | Object or Headers | Some headers are browser-controlled |
body | Sends request data | String, FormData, URLSearchParams, Blob | Not for GET or HEAD |
mode | Controls origin behavior | cors, same-origin, no-cors | no-cors produces restricted responses |
credentials | Controls cookies and authentication information | same-origin, include, omit | Cross-origin cookies require server permission |
cache | Influences the browser HTTP cache | default, no-store, reload, no-cache, force-cache, only-if-cached | Server cache headers and browser support also matter |
redirect | Controls redirect handling | follow, error, manual | Behavior can be restricted by the browser |
referrerPolicy | Controls referrer information | no-referrer, same-origin | Use an appropriate privacy policy |
signal | Connects cancellation | AbortSignal | Produced by AbortController |
integrity | Checks a resource hash | Subresource Integrity value | Most useful for trusted static resources |
keepalive | Allows limited requests during page termination | true | Request size and runtime limitations apply |
priority | Hints relative request importance | high, low, auto | Supported only in some browsers |
HTTP Methods and Sending Data
| Method | Typical purpose | Request body usage | Example API operation |
|---|---|---|---|
| GET | Retrieve data | No body; use query parameters | List products |
| POST | Create or trigger an operation | Usually has a body | Create an order |
| PUT | Replace a resource | Usually has a complete resource | Replace a profile |
| PATCH | Partially update a resource | Contains changed fields | Change a display name |
| DELETE | Remove a resource | Usually no body | Delete a comment |
GET Query Parameters
const url = new URL('/api/search', location.origin);
url.searchParams.set('q', userText);
url.searchParams.set('page', '2');
const response = await fetch(url);
URLSearchParams safely encodes user-supplied values. It can also create URL-encoded form bodies:
const body = new URLSearchParams({ email: 'person@example.test', topic: 'support' });
await fetch('/contact', { method: 'POST', body });
JSON Requests
const response = await fetch('/api/items', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ name: 'Example' })
});
Content-Type tells the server how to interpret the body. Use JSON.stringify() with application/json for JSON APIs.
FormData and File Uploads
const form = document.querySelector('form');
const data = new FormData(form);
await fetch('/api/upload', {
method: 'POST',
body: data
});
For multipart form data, do not manually set Content-Type: multipart/form-data. The browser adds the correct boundary parameter. Manually replacing it can make the server unable to separate fields and files.
Headers
Headers are metadata sent with requests and responses. Common request headers include Accept, which states preferred response formats; Content-Type, which identifies the request body format; Authorization, which carries application authentication information; and conditional headers such as If-None-Match or If-Modified-Since.
const headers = new Headers({
Accept: 'application/json',
'Content-Type': 'application/json'
});
headers.set('X-Request-ID', crypto.randomUUID());
const response = await fetch('/api/items', { headers });
console.log(response.headers.get('Content-Type'));
A plain object is also valid configuration:
fetch('/api/profile', {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${clientProvidedToken}`
}
});
Browsers forbid or control certain headers, including headers related to connection management, host selection, and some security-sensitive metadata. JavaScript cannot freely impersonate another origin or override browser networking rules.
Error Handling
| Situation | Does fetch reject? | How to detect it | Recommended handling |
|---|---|---|---|
| Network failure | Usually yes | catch | Show a connection or retry state |
| Aborted request | Yes | error.name === 'AbortError' | Treat intentional cancellation separately |
| Non-2xx HTTP response | Usually no | !response.ok or status check | Turn it into an application error |
| Malformed body | Parsing rejects | Catch around json() or another parser | Inspect content type and server output |
async function readJson(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request failed with HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API request failed', {
name: error.name,
message: error.message,
url
});
throw error;
}
}
Do not display raw server messages, stack traces, tokens, or sensitive request data to users. Give users a useful state such as “Unable to load items. Try again.” Log only safe diagnostic context.
Cancellation and Timeouts
AbortController cancels a Fetch request through its signal. This is useful when a component is removed, a route changes, or a newer search makes an older request irrelevant.
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('/api/items', { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request cancelled or timed out');
} else {
throw error;
}
} finally {
clearTimeout(timer);
}
Cleanup code should abort work that is no longer relevant. An intentional abort is not necessarily a failure to show to the user.
Cross-Origin Requests and CORS
The same-origin policy restricts browser scripts from freely reading resources from a different scheme, host, or port. CORS, or Cross-Origin Resource Sharing, is a server-controlled permission system that tells the browser which cross-origin requests may be read.
A simple cross-origin request may be sent directly if it uses an allowed method and limited request headers. Other requests cause a preflight: the browser sends an OPTIONS request describing the intended method and headers. The server must respond with suitable permission headers before the browser sends the actual request.
| Request scenario | Client setting | Required server behavior | Browser outcome if not allowed |
|---|---|---|---|
| Cross-origin readable request without cookies | Usually mode: 'cors' | Return Access-Control-Allow-Origin for the requesting origin | Response is blocked from script |
| Cross-origin request with non-simple method or headers | mode: 'cors' | Permit the preflighted method and headers, often for OPTIONS | Actual request is not sent or is blocked |
| Cross-origin cookie-authenticated request | credentials: 'include' | Allow the exact origin and return Access-Control-Allow-Credentials: true | Response is blocked; cookies may not be usable |
| Opaque no-cors request | mode: 'no-cors' | Limited server interaction | Script cannot read an ordinary API response |
no-cors is not a general CORS workaround. It produces a restricted opaque response whose status and body are not normally readable by JavaScript.
Credentials, Authentication, and Security
The credentials option controls whether cookies, HTTP authentication information, and client certificates accompany a request:
same-origin: include credentials for same-origin requests; this is the usual default.include: include credentials for cross-origin requests when browser and server policies allow them.omit: do not send credentials.
Cookie-based sessions keep the session identifier in a cookie and commonly use credentials: 'include' for cross-origin APIs. Token-based APIs often use an Authorization: Bearer ... header. A browser token must be treated as client-accessible; never embed private server credentials, cloud keys, or database secrets in browser code.
Cookie-authenticated state-changing requests require CSRF protection. Use appropriate SameSite cookie settings and server-side CSRF tokens or another robust origin-validation design. Also use HTTPS, validate input on the server, and treat response data as untrusted. Safely escape external text instead of inserting it as raw HTML.
Caching and Redirects
Fetch participates in browser HTTP caching. The cache option influences cache interaction, but server response headers such as Cache-Control, ETag, and Expires also determine behavior.
default: use normal browser cache rules.no-store: avoid using or updating the cache for the request.reload: request from the server and generally update the cache.no-cache: permit cached data only after validation with the server.force-cache: use a matching cached response when possible.only-if-cached: use cache only; browser mode restrictions apply.
Redirects are normally followed with redirect: 'follow'. Use redirect: 'error' to reject redirects, or manual for environment-specific manual handling. Redirect behavior can be restricted for security and cross-origin reasons.
Reusable Request and Response Objects
Request, Response, Headers, and Body are related Fetch interfaces. A Request stores a URL and request configuration, while a Response stores status, headers, and a body.
const request = new Request('/api/items', {
method: 'GET',
headers: { Accept: 'application/json' },
cache: 'no-cache'
});
const response = await fetch(request);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const items = await response.json();
Request and response bodies are streams and are normally consumed once. Clone an object before independent consumers read its body:
const response = await fetch('/api/items');
const copy = response.clone();
const data = await response.json();
const rawText = await copy.text();
Progressive Reading with ReadableStream
For large responses, response.body may expose a ReadableStream. A reader can process chunks progressively rather than waiting for the entire body.
const response = await fetch('/api/large-file');
if (!response.ok || !response.body) throw new Error('Streaming unavailable');
const reader = response.body.getReader();
let received = 0;
while (true) {
const { value, done } = await reader.read();
if (done) break;
received += value.byteLength;
console.log(`Received ${received} bytes`);
}
A Practical API Workflow
A reusable helper centralizes status checks, parsing, and useful error context:
async function apiRequest(url, options = {}) {
const response = await fetch(url, {
headers: { Accept: 'application/json', ...options.headers },
...options
});
const contentType = response.headers.get('Content-Type') || '';
const body = contentType.includes('application/json')
? await response.json()
: await response.text();
if (!response.ok) {
const error = new Error(`API request failed: HTTP ${response.status}`);
error.status = response.status;
error.details = body;
throw error;
}
return body;
}
async function createItem(item) {
return apiRequest('/api/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item)
});
}
A list interface should represent at least four states: loading, success with results, success with no results, and failure.
async function renderItems() {
setState({ status: 'loading' });
try {
const items = await apiRequest('/api/items');
setState({ status: items.length ? 'success' : 'empty', items });
} catch (error) {
setState({ status: 'error', message: 'Items could not be loaded.' });
}
}
For pagination, send a page number or cursor in query parameters and preserve the server's next-page value. For filtering, encode each filter with URLSearchParams. For search boxes, debounce input so requests do not start on every keystroke, then abort the previous request or associate each response with a request identifier. This prevents an older, slower response from overwriting newer results.
Debounced Search with Cancellation
let controller;
let timer;
function search(query) {
clearTimeout(timer);
if (controller) controller.abort();
timer = setTimeout(async () => {
controller = new AbortController();
const url = `/api/search?q=${encodeURIComponent(query)}`;
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const results = await response.json();
renderResults(results);
} catch (error) {
if (error.name !== 'AbortError') renderSearchError();
}
}, 250);
}
Compatibility and Runtime Differences
Fetch is available in modern browsers and in many server-side JavaScript runtimes. Browser Fetch is subject to same-origin policy, CORS, cookie rules, and browser-controlled headers. Server-side Fetch generally does not have the same browser CORS restriction and may handle credentials differently.
Check the documentation for the target browser or server runtime. Use a compatible fallback or polyfill only when an older environment that lacks Fetch must be supported.
Troubleshooting Common Problems
- 404 or 500 reaches the success path: Fetch resolved because communication completed. Check
response.okbefore parsing. - CORS error: Configure the API server to allow the origin, method, headers, and credentials. Do not rely on
no-cors. response.json()fails: The body may be empty, malformed, HTML, or already consumed. Inspect status andContent-Type.- Unsupported media type: Match the body to its header. Use JSON serialization with
application/json, or use FormData without manually setting its multipart header. - Cookies are absent: Check
credentials, server CORS headers, and cookie SameSite and Secure attributes. - Unexpected abort: Check timeout and cleanup paths, and distinguish
AbortErrorfrom other failures. - Old search results appear: Abort stale requests or verify a request identifier before updating state.
- Server code works but browser code fails: Test in the browser context and configure the API for browser CORS and cookie behavior.
Fetch Outcome Checklist
- Confirm the URL and HTTP method.
- Set headers that describe the body and desired response.
- Serialize JSON with
JSON.stringify(). - Use FormData for multipart forms and let the browser set the boundary.
- Check
response.okor an expected status. - Choose the correct body parser.
- Catch network, abort, and parsing failures.
- Update loading, success, empty, and error states.
- Cancel work that is no longer relevant.
- Keep secrets out of browser code and configure CORS on the server.
See the Fetch API reference path for related API-oriented material.