Skip to content

HttpClient

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

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

name is required. All other fields are optional — pass only what you need.

PropertyTypeDefaultDescription
namestringrequiredName for the client instance
cacheHttpClientCacheOptionsCache configuration (see below)
dedupeDedupeStore<T>Request deduplication
rateLimitHttpClientRateLimitOptionsRate limit configuration (see below)
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
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
retryRetryOptions | falseAutomatic retry configuration. See Retries guide
PropertyTypeDefaultDescription
storeCacheStore<T>requiredCache store instance
globalScopebooleanfalseWhen true, cache keys are not prefixed with the client name. By default, keys are prefixed with name: to isolate each client’s cache entries
ttlnumber3600Cache TTL in seconds. Used when response has no cache headers
overridesCacheOverrideOptionsOverride specific cache header behaviors (see below)

rateLimit Options (HttpClientRateLimitOptions)

Section titled “rateLimit Options (HttpClientRateLimitOptions)”
PropertyTypeDefaultDescription
storeRateLimitStore | AdaptiveRateLimitStoreRate limit store instance (optional — server cooldown logic works without a store)
throwbooleantrueThrow when rate limited vs. wait
maxWaitTimenumber60000Max wait time in ms before throwing
headersRateLimitHeaderConfigdefaultsConfigure standard/custom header names
resourceExtractor(url: string) => stringURL originExtract rate-limit resource key from URL
configsRateLimitConfigMapPer-resource rate limit configurations
defaultConfigRateLimitConfigFallback rate limit config when no per-resource config matches

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
cache{ ttl?: number; overrides?: CacheOverrideOptions; tags?: string[] }Per-request cache options. ttl overrides constructor TTL; overrides are shallow-merged with constructor-level; tags associates the cached entry with tags for later invalidation

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({
name: 'my-api',
rateLimit: {
headers: {
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)

Invalidates all cache entries associated with the given tag. Returns the number of entries removed.

const count = await client.invalidateByTag('users');

Invalidates all cache entries associated with any of the given tags. Returns the number of unique entries removed.

const count = await client.invalidateByTags(['users', 'user:123']);

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

await client.flushRevalidations();
const client = new HttpClient({ name: 'my-api', cache: { store: new InMemoryCacheStore() } });
const client = new HttpClient({
name: 'my-api',
cache: { store: new SQLiteCacheStore({ database: db }), ttl: 600 },
dedupe: new SQLiteDedupeStore({ database: db }),
rateLimit: {
store: new SqliteAdaptiveRateLimitStore({
database: db,
defaultConfig: { limit: 200, windowMs: 3_600_000 },
}),
throw: 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({
name: 'my-api',
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({
name: 'my-api',
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({
name: 'my-api',
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;
},
});