v1.6.x-dev NDS IQ v6

Request - National Design System

A fetch wrapper that applies a timeout, a response size cap, and a status check to every call, then hands back parsed JSON or raw text. Use it anywhere you would reach for fetch so a hung endpoint or an oversized response cannot leave your UI stuck.

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.

Timeout by default

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.

Response size cap

Bodies are streamed and cancelled the moment they pass maxBytes, so an unexpectedly huge response is refused rather than parsed.

Abort composition

Pass your own signal and it is combined with the timeout, so a superseding request and an expiry both cancel cleanly through one channel.

JSON or text, decided for you

The Content-Type header picks the branch, and json: true forces parsing when you know the endpoint better than its headers do.

Errors you can branch on

Failures carry status and name, so retry logic never has to match on a message string.

Every fetch option still works

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.

Read JSON from an endpoint
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.

OptionDefaultDescription
timeout15000Milliseconds before the request aborts. Set 0 to opt out entirely, for a long poll or a stream.
maxBytes1048576Ceiling on the response body, in bytes. Raise it for HTML fragments, which run larger than JSON payloads.
jsonsniffedForces 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.
signalnoneYour 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.

PropertyTypeDescription
isJsonbooleanWhether the body was treated as JSON.
dataobject | stringParsed JSON, or the raw text when it is not JSON. An empty body yields an empty string.
FailureHow to detect it
Non-OK statuserror.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 reachederror.name === 'TimeoutError'
Aborted by your signalerror.name === 'AbortError'. Usually means a newer request replaced this one, so most callers stay silent here.
Over maxBytesNeither 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.

Cancel a request that a newer one replaces
let controller; async function search(term) { if (controller) controller.abort(); controller = new AbortController(); const { signal } = controller; setLoading(true); try { const { data } = await NDS.request(`/api/search?q=${encodeURIComponent(term)}`, { signal, json: true }); renderResults(data); } catch (error) { // A newer search aborted this one. It owns the loading state now, // so clearing it here would kill a spinner that is still needed. if (error.name === 'AbortError') return; showError(); } finally { if (controller.signal === signal) setLoading(false); } }
Leave the page consistent when a request fails
// Taking over Filter's AJAX submission. Chips, badge and URL params are // already committed when this fires, so a failed request has to put them // back: otherwise they describe results that were never rendered. filterEl.addEventListener('nds:filterFormAjax', (e) => { e.preventDefault(); const params = new URLSearchParams(new FormData(e.detail.form)); NDS.request(`/api/search?${params}`, { json: true }) .then(({ data }) => renderResults(data.Records)) .catch(() => e.detail.rollback()); });

Usage Guidelines

Best Practices

  • Reach for it wherever you would call fetch to read a response. The guards it adds are the ones every caller eventually needs and rarely writes.
  • Pass json: true whenever you know the endpoint returns JSON. Static hosts and misconfigured servers label JSON as text/plain often enough that trusting the header silently hands you a string.
  • Raise maxBytes for HTML fragments. The default suits JSON payloads, and a full page of markup can legitimately exceed it.
  • Treat AbortError as 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.status and error.name, never on the message text. Messages change; those two do not.
  • Guard whatever you release in a finally block. 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: 0 only 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.

NDS.request(url, options) → Promise<{ isJson, data }> // options // timeout number ms before abort (default 15000, 0 disables) // maxBytes number response ceiling in bytes (default 1048576) // json boolean force the JSON branch (default: sniff Content-Type) // signal AbortSignal your own signal, combined with the timeout // ...rest forwarded to fetch (method, headers, body, credentials, cache, …) // resolves // isJson boolean whether the body was treated as JSON // data object | string parsed JSON, or raw text // rejects // error.status HTTP code on a non-OK response // (error.url and error.body — first ~512 bytes of the // response body, undefined if the read failed — set too) // error.name 'TimeoutError' the timeout elapsed // error.name 'AbortError' your signal aborted, usually a superseding request // neither the response exceeded maxBytes // POST a form and read an HTML fragment back const { isJson, data } = await NDS.request('/api/search', { method: 'POST', body: new FormData(form), maxBytes: 4194304 });
Last Modified Date: 29/07/2026 - 03:35 PM
Was this page useful?
60% of users said Yes from 2843 Feedbacks