Overview
NDS.request() ships in the main bundle and is available on every page. It owns the part of a request that is easy to get wrong: aborting on time, refusing a response that is too large, throwing on a non-OK status, and deciding whether the body is JSON. Every component in the system that fetches goes through it.
Every call aborts after 15 seconds unless you set your own. A stalled endpoint surfaces as a rejection instead of a spinner that never stops.
Bodies are streamed and cancelled the moment they pass maxBytes, so an unexpectedly huge response is refused rather than parsed.
Pass your own signal and it is combined with the timeout, so a superseding request and an expiry both cancel cleanly through one channel.
The Content-Type header picks the branch, and json: true forces parsing when you know the endpoint better than its headers do.
Failures carry status and name, so retry logic never has to match on a message string.
Anything you would pass to fetch, including method, headers, body, and credentials, is forwarded untouched.
Example
The default shape: read JSON, render on success, branch on error.status for known failure modes.
try {
const { data } = await NDS.request('/api/services', { json: true });
renderServices(data);
} catch (error) {
if (error.status === 404) return showEmptyState();
showError();
}
Options
Four options belong to the helper. Everything else in the object is handed to fetch as-is, so any option fetch supports now or gains later works without a change here. The trade is a shared name space: these four names are ones fetch can never use, which is why they stay deliberately outside its vocabulary.
| Option | Default | Description |
|---|---|---|
timeout | 15000 | Milliseconds before the request aborts. Set 0 to opt out entirely, for a long poll or a stream. |
maxBytes | 1048576 | Ceiling on the response body, in bytes. Raise it for HTML fragments, which run larger than JSON payloads. |
json | sniffed | Forces the JSON branch on or off. Without it the Content-Type header decides. Pass true when the endpoint returns JSON but the host may mislabel it. |
signal | none | Your own AbortSignal, combined with the timeout rather than replacing it. |
Result and Failures
A resolved call returns an object, so it can grow new fields later without breaking callers. A rejected call throws an Error you can classify without reading its message.
| Property | Type | Description |
|---|---|---|
isJson | boolean | Whether the body was treated as JSON. |
data | object | string | Parsed JSON, or the raw text when it is not JSON. An empty body yields an empty string. |
| Failure | How to detect it |
|---|---|
| Non-OK status | error.status holds the HTTP code, error.url the request URL, and error.body a best-effort slice of the response body (first ~512 bytes, undefined if the read failed). Surface it in the toast or log so the operator sees what the server actually said. |
| Timeout reached | error.name === 'TimeoutError' |
| Aborted by your signal | error.name === 'AbortError'. Usually means a newer request replaced this one, so most callers stay silent here. |
Over maxBytes | Neither a status nor a recognised name is present. |
Usage
Two more shapes cover the rest: a read a later interaction can cancel, and a read whose failure has to leave the page consistent.
Usage Guidelines
Best Practices
- Reach for it wherever you would call
fetchto read a response. The guards it adds are the ones every caller eventually needs and rarely writes. - Pass
json: truewhenever you know the endpoint returns JSON. Static hosts and misconfigured servers label JSON astext/plainoften enough that trusting the header silently hands you a string. - Raise
maxBytesfor HTML fragments. The default suits JSON payloads, and a full page of markup can legitimately exceed it. - Treat
AbortErroras silent. It means a newer request replaced this one, so showing an error would report a failure the user did not experience. - Branch on
error.statusanderror.name, never on the message text. Messages change; those two do not. - Guard whatever you release in a
finallyblock. When a superseding request has already taken over the loading state, clearing it there hides a spinner that is still needed. - Do not use it for uploads that need progress events. Those require
XMLHttpRequest, which is what Upload uses. - Best-effort widget that wants a fallback instead of a throw on non-OK? A one-line wrapper at the call site is enough — no option needed here:
const optional = (url, opts) => NDS.request(url, opts).catch(err => err.status ? null : Promise.reject(err)); - Do not wrap it in a retry helper without checking the request is safe to repeat. A filter or form submission may not be idempotent.
- Set
timeout: 0only for a connection meant to stay open. Every ordinary request is better off failing than hanging.
What it does not do
The helper owns the response contract and nothing else, which keeps it predictable across every component that calls it. It does not manage loading state, apply a response to the DOM, or build the request for you. It adds no retry, no caching, and no interceptors: pass cache: 'default' through to fetch and the browser HTTP cache handles repeat reads.
JavaScript API
Available on every page as part of the main bundle. No initialization required.