Outcome Recipes
Scenario-based patterns for @aurelia/fetch-client that solve common API challenges like auth, caching, and uploads.
Use these patterns when you know the experience you want - resilient API calls, cache-friendly dashboards, or abortable uploads - and need the exact configuration steps.
1. Token-aware API client with automatic retries
Goal: Add an Authorization header to every request, refresh expired tokens, and retry transient 5xx errors with exponential backoff.
Steps
Configure the shared
IHttpClientonce during app startup:import { IHttpClient } from '@aurelia/fetch-client'; import { resolve } from '@aurelia/kernel'; const http = resolve(IHttpClient); http.configure(config => config .withBaseUrl('https://api.example.com/') .withDefaults({ headers: { 'Content-Type': 'application/json' } }) .withRetry({ maxRetries: 3, strategy: RetryStrategy.exponential, interval: 2000, doRetry: (error) => error.status >= 500 }) .withInterceptor({ request(request) { request.headers.set('Authorization', `Bearer ${tokenStore.current}`); return request; }, async responseError(error, request, client) { if (error.status === 401) { await tokenStore.refresh(); return client.fetch(request); } throw error; } }) .rejectErrorResponses() );Use helper methods (
http.get,http.post, etc.) everywhere else - each call now benefits from the shared interceptors and retry policy.
Checklist
Expired tokens trigger exactly one refresh and replay of the original request.
5xx responses retry with exponential delays, verified by watching network logs.
http.rejectErrorResponses()makes failed HTTP status codes reject Promises so callers cancatchthem.
2. Stale-while-revalidate dashboards
Goal: Cache GET responses for a few minutes, immediately serve stale data if needed, and refresh in the background so dashboards feel instant.
Steps
Instantiate the cache interceptor via DI (it needs access to
ICacheService):import { CacheInterceptor } from '@aurelia/fetch-client'; import { resolve } from '@aurelia/kernel'; const cacheInterceptor = resolve(CacheInterceptor, [{ cacheTime: 300_000, // keep entries for 5 minutes staleTime: 30_000, // mark as stale after 30s refreshStaleImmediate: true, refreshInterval: 60_000 // background refresh every minute }]);Register the interceptor when configuring the client:
http.configure(config => config.withInterceptor(cacheInterceptor));Fetch dashboards via
http.get('/dashboards/overview')- the interceptor handles cache reads or writes.
Checklist
First load hits the network; subsequent loads within
cacheTimeresolve instantly.When the cache is stale, users see cached data immediately and fresh data appears shortly after (refresh interval logs confirm the background fetch).
Non-GET requests bypass the cache automatically.
3. Abortable uploads with progress feedback
Goal: Let users cancel long uploads and show progress using the same client instance.
Steps
Build a wrapper that creates an
AbortControllerper upload:export class UploadService { private http = resolve(IHttpClient); upload(file: File, onProgress: (percent: number) => void) { const controller = new AbortController(); const body = new FormData(); body.append('file', file); const request = new Request('/files', { method: 'POST', body, signal: controller.signal }); const trackedUpload = this.http.fetch(request) .then(response => response.json()); return { cancel: () => controller.abort(), ready: trackedUpload }; } }Use the browser’s upload progress events (e.g., XHR or a custom uploader) if you need granular progress numbers; Fetch itself does not emit upload progress, but you can still expose a spinner tied to
http.isRequestingor theactiveRequestCount.
Checklist
Calling
cancel()triggersAbortError, which you can catch to reset UI state.http.isRequestingflips totruewhile uploads run, so global loading indicators stay accurate.Multiple uploads in parallel each get their own controller, preventing accidental cross-cancellation.
4. Time-boxed requests with automatic timeout
Goal: Cancel any request that takes longer than a threshold (for example, 8 seconds) and show a friendly timeout message.
Steps
Wrap
http.fetchso each call gets anAbortControllerplus a timeout:function fetchWithTimeout(input: RequestInfo, init: RequestInit = {}, timeoutMs = 8000) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); const request = new Request(input, { ...init, signal: controller.signal }); return resolve(IHttpClient).fetch(request) .finally(() => clearTimeout(timeout)); }Use it wherever you need snappy UX:
try { const response = await fetchWithTimeout('/reports/slow'); return await response.json(); } catch (error) { if (error.name === 'AbortError') { toast.show('Request timed out, please try again'); } throw error; }
Checklist
Requests that exceed the timeout trigger
AbortErrorand can be handled centrally.Successful responses clear the timeout to avoid leaks.
Global loading indicators drop immediately after the abort because
http.isRequestingreturns tofalse.
5. Correlated requests with logging
Goal: Attach a unique correlation ID to every request for troubleshooting and log response times without sprinkling code across services.
Steps
Add an interceptor that stamps IDs and measures duration (store timings in a
WeakMapso you can read them later):const timings = new WeakMap<Request, { id: string; start: number }>(); http.configure(config => config.withInterceptor({ request(request) { const id = crypto.randomUUID?.() ?? `${Date.now()}-${Math.random()}`; const start = performance.now(); timings.set(request, { id, start }); request.headers.set('X-Request-Id', id); return request; }, response(response, request) { const timing = request ? timings.get(request) : null; if (request && timing) { const elapsed = (performance.now() - timing.start).toFixed(1); console.info(`[${timing.id}] ${response.status} ${response.url} in ${elapsed}ms`); timings.delete(request); } return response; } }));If your server needs to read the correlation ID, emit the header from the request before forwarding it upstream.
Consume the logs in your monitoring system or browser console.
Checklist
Every network call carries an
X-Request-Idheader visible in browser dev tools.Logs show the correlation ID, status, URL, and elapsed time.
The interceptor lives in one place, so adding fields (tenant, locale) is straightforward.
Pattern picker
Resilient, authenticated API calls
withInterceptor, withRetry, rejectErrorResponses
Instant dashboards with background refresh
CacheInterceptor, custom cache configuration, withInterceptor
Abortable uploads
AbortController, http.fetch, http.isRequesting
Time-boxed requests
AbortController, manual timeout wrapper
Correlated logging
Custom interceptors, request headers
See the rest of the fetch-client docs for deep dives:
Last updated
Was this helpful?