Skip to content

HttpClient

The main client class that orchestrates caching, deduplication, and rate limiting.

import { HttpClient } from '@http-client-toolkit/core';
new HttpClient(stores?, options?)

All stores are optional. Pass only the ones you need.

PropertyTypeDescription
cacheCacheStore<T>Response caching
dedupeDedupeStore<T>Request deduplication
rateLimitRateLimitStore | AdaptiveRateLimitStoreRate limiting
PropertyTypeDefaultDescription
fetchFn(url: string, init?: RequestInit) => Promise<Response>globalThis.fetchCustom fetch implementation
requestInterceptor(url: string, init: RequestInit) => Promise<RequestInit> | RequestInitPre-request hook to modify the outgoing request
responseInterceptor(response: Response, url: string) => Promise<Response> | ResponsePost-response hook to inspect/modify the raw Response
defaultCacheTTLnumber3600Cache TTL in seconds
throwOnRateLimitbooleantrueThrow when rate limited vs. wait
maxWaitTimenumber60000Max wait time in ms before throwing
responseTransformer(data: unknown) => unknownTransform parsed response data before caching (e.g. snake_case to camelCase)
responseHandler(data: unknown) => unknownPost-transformation hook for validation or domain-level error detection. Throw to reject 2xx responses with application-level errors
errorHandler(context: HttpErrorContext) => ErrorConvert HTTP errors to domain-specific types. Context includes url, response status, parsed data, and headers. Not called for network failures
rateLimitHeadersRateLimitHeaderConfigdefaultsConfigure standard/custom header names
retryRetryOptions | falseAutomatic retry configuration. See Retries guide
cacheOverridesobjectOverride specific cache header behaviors (see below)

Makes a GET request through the configured pipeline.

const data = await client.get<{ name: string }>(
'https://api.example.com/user/1',
);

The url must be an absolute URL (e.g. https://api.example.com/items).

Request Options

PropertyTypeDefaultDescription
signalAbortSignalCancels wait + request when aborted
priority'user' | 'background''background'Used by adaptive rate-limit stores
headersRecord<string, string>Custom headers sent with the request; also used for Vary-based cache matching
retryRetryOptions | falsePer-request retry override. Pass false to disable retries for this request

When client.get(url) is called, the request passes through each configured layer:

  1. Cache — Return cached response if available
  2. Dedupe — If an identical request is already in-flight, wait for its result
  3. Rate Limit — Wait or throw if the rate limit is exceeded
  4. Request Interceptor — Modify the outgoing request (e.g. inject auth headers)
  5. Fetch — Execute the HTTP request via fetchFn (or globalThis.fetch)
  6. Response Interceptor — Inspect or modify the raw Response
  7. Retry — On transient failure, repeat steps 4–6 with exponential backoff (if configured)
  8. Transform & Validate — Apply responseTransformer then responseHandler
  9. Store — Cache the result, record the rate limit hit, and resolve any deduplicated waiters

See the Interceptors guide for detailed usage.

Consistent across all built-in stores:

ValueBehavior
ttlSeconds > 0Expires after N seconds
ttlSeconds === 0Never expires (permanent)
ttlSeconds < 0Immediately expired

The client respects these headers out of the box:

  • Retry-After
  • RateLimit-Remaining / RateLimit-Reset
  • X-RateLimit-Remaining / X-RateLimit-Reset
  • Rate-Limit-Remaining / Rate-Limit-Reset
  • Combined structured RateLimit (e.g. "default";r=0;t=30)

Cooldowns are enforced when:

  • The response is a throttling status (429 or 503), or
  • Remaining quota is explicitly exhausted (remaining <= 0)
const client = new HttpClient(stores, {
rateLimitHeaders: {
retryAfter: ['RetryAfterSeconds'],
remaining: ['Remaining-Requests'],
reset: ['Window-Reset-Seconds'],
},
});

The client respects Cache-Control, ETag, Last-Modified, and Expires headers per RFC 9111. See the Caching guide for details.

PropertyTypeDescription
ignoreNoStorebooleanCache responses even when no-store is set
ignoreNoCachebooleanSkip revalidation even when no-cache is set
minimumTTLnumberFloor on header-derived freshness (seconds)
maximumTTLnumberCap on header-derived freshness (seconds)

Waits for all pending stale-while-revalidate background fetches to complete. Useful in tests.

await client.flushRevalidations();
const client = new HttpClient({ cache: new InMemoryCacheStore() });
const client = new HttpClient(
{
cache: new SQLiteCacheStore({ database: db }),
dedupe: new SQLiteDedupeStore({ database: db }),
rateLimit: new SqliteAdaptiveRateLimitStore({
database: db,
defaultConfig: { limit: 200, windowMs: 3_600_000 },
}),
},
{
defaultCacheTTL: 600,
throwOnRateLimit: false,
maxWaitTime: 30_000,
},
);
const controller = new AbortController();
const data = await client.get(url, { signal: controller.signal });
// Cancel from elsewhere
controller.abort();
const client = new HttpClient(stores, {
requestInterceptor: async (url, init) => {
const token = await getAccessToken();
const headers = new Headers(init.headers);
headers.set('Authorization', `Bearer ${token}`);
return { ...init, headers };
},
responseInterceptor: (response, url) => {
console.log(`${response.status} ${url}`);
return response;
},
});
const client = new HttpClient(stores, {
fetchFn: async (url, init) => {
const response = await fetch(url, init);
// Follow pre-signed URL redirects before caching
if (response.headers.has('x-redirect-url')) {
return fetch(response.headers.get('x-redirect-url')!, init);
}
return response;
},
});
import camelcaseKeys from 'camelcase-keys';
const client = new HttpClient(stores, {
responseTransformer: (data) =>
camelcaseKeys(data as Record<string, unknown>, { deep: true }),
responseHandler: (data) => {
if (!data || typeof data !== 'object') {
throw new Error('Unexpected response shape');
}
return data;
},
});