This document provides comprehensive context for AI assistants working with the axios-cache-interceptor codebase.
axios-cache-interceptor is a high-performance, feature-rich caching layer for Axios HTTP client. It intercepts requests and responses to provide intelligent caching with minimal configuration.
- Prevent redundant network requests through smart caching
- Handle concurrent requests efficiently (request deduplication)
- Support HTTP caching standards (Cache-Control, ETag, Vary, etc.)
- Provide flexible storage backends (memory, localStorage, custom)
- Work seamlessly in both browser and Node.js environments
- Automatic request deduplication: Concurrent identical requests share a single network call
- HTTP standard compliance: Respects Cache-Control, ETag, If-None-Match, If-Modified-Since, Vary headers
- Stale-while-revalidate: Serve stale cache while fetching fresh data
- Flexible storage: Built-in memory and web storage, extensible for Redis, IndexedDB, etc.
- Per-request configuration: Override global cache settings per request
- TypeScript-first: Fully typed with excellent IDE support
- Development builds: Separate debug builds with comprehensive logging
src/
├── cache/ # Core cache types and setup
│ ├── axios.ts # Extended Axios types (AxiosCacheInstance, CacheAxiosResponse)
│ ├── cache.ts # Cache configuration interfaces (CacheProperties, CacheInstance)
│ └── create.ts # setupCache() - main entry point
├── interceptors/ # Request/response interception logic
│ ├── request.ts # Request interceptor (cache lookup, concurrent handling)
│ ├── response.ts # Response interceptor (cache storage, error handling)
│ ├── util.ts # Shared interceptor utilities
│ └── build.ts # Interceptor types
├── storage/ # Storage adapters
│ ├── types.ts # StorageValue types (cached/stale/loading/empty)
│ ├── build.ts # buildStorage() - storage builder
│ ├── memory.ts # In-memory storage with eviction
│ └── web-api.ts # localStorage/sessionStorage adapter
├── header/ # HTTP header interpretation
│ ├── interpreter.ts # Parse Cache-Control, Expires
│ ├── extract.ts # Extract headers for Vary support
│ └── headers.ts # Header name constants
├── util/ # Utilities
│ ├── key-generator.ts # Request ID generation (defaultKeyGenerator)
│ ├── cache-predicate.ts # Response cacheability tests
│ ├── update-cache.ts # Cache invalidation/updates
│ └── types.ts # Shared types
└── index.ts # Public API exports
test/ # Test suite (node:test)
docs/ # VitePress documentation
Request Flow:
User Request
→ Request Interceptor
→ Generate Request ID (key)
→ Check cache (storage.get)
→ If cached: Return cached response
→ If loading: Wait for concurrent request
→ If empty/stale: Make network request
→ Network (Axios adapter)
→ Response Interceptor
→ Interpret headers (TTL, ETag, etc.)
→ Test cache predicate
→ Store in cache (storage.set)
→ Resolve waiting concurrent requests
→ User receives response
Every cache entry has one of these states:
empty: No cached data existscached: Valid cached data within TTLstale: Expired data that can be revalidated (ETag/Last-Modified)must-revalidate: Cache-Control: must-revalidate (similar to stale but stricter)loading: Request in progressprevious: 'empty': First requestprevious: 'stale': Revalidating stale dataprevious: 'must-revalidate': Revalidating with must-revalidate
Each request gets a unique ID (cache key) generated by KeyGenerator:
- Uses
config.idif provided (manual override) - Otherwise generates hash from:
method,baseURL,url,params,data - With Vary support: Also includes subset of request headers
- Uses
object-codelibrary for 32-bit hash (collision risk at ~77k keys)
Critical for performance:
- First request sets cache state to
loading - Creates
Deferredpromise inwaitingMap - Subsequent identical requests wait on this deferred
- When response arrives, resolves/rejects all waiting requests
- Prevents multiple network calls for same resource
HTTP Vary header support for content negotiation:
- Server sends
Vary: Authorization, Accept-Language - Library extracts specified headers from request
- Includes them in cache key (via metadata)
- Different header values = different cache entries
Vary: *immediately marks cache as stale (must revalidate every time)
Support for ETag and If-Modified-Since:
- ETag: Stores response ETag, sends
If-None-Matchon revalidation - Last-Modified: Stores Last-Modified or cache timestamp, sends
If-Modified-Since - 304 Not Modified: Keeps existing cache data, updates timestamps
- Enabled by default, controlled by
cache.etagandcache.modifiedSince
Optimistic UI updates:
- When making network request but stale cache exists
- Calls
cache.hydrate(staleData)immediately - Developer can update UI with old data
- Network response updates UI with fresh data
- Prevents UI flicker on slow networks
Entry point for library. Takes Axios instance and returns enhanced instance.
Key responsibilities:
- Validates single initialization (prevents double setup)
- Initializes storage (defaults to memory)
- Sets up interceptors
- Merges global and per-request config
- Sets default cache properties
Default configuration:
ttl: 5 minutesmethods: ['get', 'head']cachePredicate: RFC 7231 cacheable status codesinterpretHeader: trueetag: truemodifiedSince: false (unless etag disabled)vary: truestaleIfError: true
Most complex logic in the library.
Key operations:
- Generate request ID
- Check if caching disabled (
cache: falseorenabled: false) - Check URL filters (
ignoreUrls,allowUrls) - Apply
cacheTakeoverheaders (prevent browser cache) - Validate method against
cache.methods - Get cached value
- Handle Vary mismatches (regenerate key if needed)
- Handle concurrent requests (wait on deferred)
- Set up loading state for new requests
- Add conditional headers for stale revalidation
- Call
hydratecallback if available
Critical sections:
- Concurrent request detection: Checks
axios.waitingMap - Vary mismatch handling: Switches to new cache key mid-flight
- Custom adapter injection: Returns cached data without network
Handles successful and failed responses.
Key operations:
- Skip if already cached (from request interceptor)
- Update other caches (
cache.update) - Test cache predicate (should this be cached?)
- Interpret headers for TTL (Cache-Control, Expires)
- Store cache metadata (ETag, Last-Modified, Vary headers)
- Handle
Vary: *(immediately mark as stale) - Resolve waiting concurrent requests
- Error handling: Implement stale-if-error
Error interceptor logic:
- Check if cache should be returned despite error
- Respect
cache.staleIfErrorconfiguration - Can use stale cache for network errors
- Re-marks cache as stale for future requests
Critical type definitions. All storage implementations must follow this interface.
Key types:
StorageValue: Union of all possible statesCachedResponse: Stored response data structureCachedResponseMeta: Metadata (vary headers, revalidation info)AxiosStorage: Interface for storage implementations
Storage methods:
get(key): ReturnsStorageValue(never undefined, returns{state: 'empty'})set(key, value): StoresNotEmptyStorageValueremove(key): Deletes cache entryclear(): Optional, clears all (not used by interceptor)
Default storage implementation.
Features:
- JavaScript
Mapfor O(1) lookups - Optional data cloning (
cloneoption) to prevent mutation - Automatic cleanup interval
- FIFO eviction with
maxEntries maxStaleAgeto prevent memory leaks from stale entries- Smart eviction ordering (prefers expired over stale over cached)
Gotchas:
- Without cloning, mutations affect cached data
- Long-running processes need cleanup interval
- Default 1024 entry limit may need adjustment
localStorage/sessionStorage adapter.
Features:
- JSON serialization
- Key prefixing to avoid collisions
- Quota exceeded handling (auto-eviction)
maxStaleAgefor cleanup
Eviction strategy on quota exceeded:
- Remove all expired entries
- Remove oldest entry
- Retry save
- Repeat until success or storage empty
Generates unique IDs for cache keys.
Default generator considers:
method(lowercase normalized)baseURL(trailing slashes removed)url(trailing slashes removed)params(query parameters)data(request body)meta.vary(Vary header values)
Uses object-code library for hashing:
- 32-bit signed integer hash
- Fast but collision risk at ~77k unique requests
- Custom generator recommended for large-scale persistent cache
Interprets HTTP caching headers.
Implements:
- Cache-Control: max-age, s-maxage, no-cache, no-store, must-revalidate, private, public
- Expires: Absolute expiration date
- Age: Response age from proxy
- stale-if-error: Grace period for errors
- stale-while-revalidate: Background revalidation
Returns:
'dont cache': No caching (no-cache, no-store)'not enough headers': Use TTL from confignumber: TTL in milliseconds{ cache: number, stale: number }: TTL and stale TTL
All options in CacheProperties can be set globally:
const axios = setupCache(instance, {
// Cache defaults for all requests
ttl: 1000 * 60 * 15,
methods: ['get', 'head', 'post'],
storage: buildMemoryStorage(),
debug: console.log,
// Less common options
generateKey: customKeyGenerator,
headerInterpreter: customInterpreter,
cacheTakeover: true,
interpretHeader: true,
vary: true,
staleIfError: true,
etag: true,
modifiedSince: false
});Override global settings per request:
await axios.get('/api/data', {
// Custom request ID
id: 'my-custom-id',
cache: {
// Override any global option
ttl: 1000 * 60 * 60,
enabled: false,
override: true,
// Per-request specific
update: { [otherRequestId]: 'delete' },
hydrate: async (cache) => updateUI(cache.data),
cachePredicate: {
statusCheck: [200, 201],
responseMatch: (res) => res.data.cacheable,
ignoreUrls: [/\/admin/],
allowUrls: ['/api/public']
}
}
});- Uses Node.js built-in
node:test(no external framework) - Assertions via
node:assert - Located in
test/**/*.test.ts
test/mocks/axios.js: Mock Axios instance
- Returns
mockAxios(options)- pre-configured instance - Includes mock adapter that returns successful responses
- Pre-wrapped with
setupCache()
test/utils.ts: Test helpers
mockDateNow(): Mock Date.now() for time-based tests- Cleanup utilities
npm test # Run all tests with coverage
npm run test:only # Run tests marked with .only
npm run test:types # TypeScript type checkingConcurrent request testing:
const [resp1, resp2] = await Promise.all([
axios.get('http://test.com'),
axios.get('http://test.com')
]);
assert.equal(resp1.cached, false);
assert.ok(resp2.cached);Storage state testing:
const response = await axios.get('url');
const cache = await axios.storage.get(response.id);
assert.equal(cache.state, 'cached');Cache invalidation testing:
const cacheKey = axios.generateKey({
url: '/api/data',
method: 'get'
});
await axios.storage.remove(cacheKey);
const cache = await axios.storage.get(cacheKey);
assert.equal(cache.state, 'empty');- Understand the interceptor flow - Most features touch request/response interceptors
- Consider storage state transitions - How does feature affect state machine?
- Think about concurrent requests - Will feature work with deduplication?
- HTTP compliance - Check RFC specs for caching behavior
- Add tests - Cover normal case, edge cases, concurrent scenarios
- Update TypeScript types - Maintain type safety
- Document in VitePress - Add to docs/src/
Request interceptor (src/interceptors/request.ts):
- Runs before network request
- Critical: Handle cache state transitions carefully
- Must update
waitingMap correctly - Consider Vary header implications
- Don't break concurrent request handling
Response interceptor (src/interceptors/response.ts):
- Runs after network response
- Always resolve/reject deferred promises
- Clean up
waitingMap - Handle both success and error paths
- Consider cache predicate carefully
To create custom storage:
import { buildStorage } from 'axios-cache-interceptor';
const myStorage = buildStorage({
async find(key) {
// Return StorageValue or undefined
},
async set(key, value) {
// Store value
// Handle TTL/expiration if backend supports it
},
async remove(key) {
// Delete key
},
async clear() {
// Optional: clear all keys
}
});Important considerations:
find()should handle expired entries (return undefined or correct state)set()receivesvalue.statewhich can be 'loading', 'cached', 'stale'- For auto-expiring backends (Redis), calculate expiration time carefully
- Stale entries should live longer than fresh TTL (they're revalidatable)
Custom header interpreter:
import {
setupCache,
type HeaderInterpreter
} from 'axios-cache-interceptor';
const myInterpreter: HeaderInterpreter = (headers, location) => {
// Return: 'dont cache' | 'not enough headers' | number | {cache: number, stale: number}
if (headers['x-custom-cache']) {
return Number(headers['x-custom-cache']) * 1000;
}
return 'not enough headers'; // Fall back to config.ttl
};Custom key generator for better collision resistance:
import { buildKeyGenerator } from 'axios-cache-interceptor';
import { createHash } from 'crypto';
const customGenerator = buildKeyGenerator((req, meta) => {
// Build unique string
const str = `${req.method}:${req.url}:${JSON.stringify(req.params)}`;
// Use stronger hash (for persistent storage)
return createHash('sha256').update(str).digest('hex');
});
setupCache(axios, { generateKey: customGenerator });// Disable cache by default
const axios = setupCache(instance, {
enabled: false
});
// Enable for specific endpoints
axios.get('/expensive-api', {
cache: { enabled: true, ttl: 1000 * 60 * 10 }
});// Delete specific cache entry
const response = await axios.get('/api/users');
await axios.storage.remove(response.id);
// Delete by custom ID
await axios.storage.remove('my-custom-id');
// Clear all cache
await axios.storage.clear?.();
// Update multiple caches after mutation
axios.post('/api/users', userData, {
cache: {
update: {
'user-list': 'delete',
'user-count': 'delete'
}
}
});// Use development build
import { setupCache } from 'axios-cache-interceptor/dev';
const axios = setupCache(instance, {
debug: (obj) => {
console.log(`[${obj.id}] ${obj.msg}`, obj.data);
}
});Debug output shows:
- Cache hits/misses
- Concurrent request handling
- Header interpretation
- Vary mismatches
- Error handling decisions
Vary: * means response varies by everything (uncacheable in shared caches).
- Library marks these as immediately stale
- Forces revalidation every time
- Storage state:
stalewith immediate expiration
Two requests with different headers wait on same initial request:
- Request A (Authorization: Bearer A) starts
- Request B (Authorization: Bearer B) waits
- When A completes, B checks vary headers
- B detects mismatch, makes own request (doesn't use A's cache)
Cache entry evicted while request in-flight:
- Request sets state to
loading - Storage runs eviction (LRU/quota)
- Response arrives, storage state is
empty - Interceptor handles gracefully (no-op waiting Map cleanup)
Axios request cancelled mid-flight:
- Request interceptor started, waiting Map has deferred
- User cancels request
- Response interceptor sees
ERR_CANCELED - Must not clear cache if already cached
- Must reject deferred to unblock concurrent requests
Server returns 304:
- Request sent with
If-None-MatchorIf-Modified-Since - Server validates, returns 304 (no body)
- Keep existing cached data
- Update timestamps (extends TTL)
- Special handling in response interceptor
- tsdown: TypeScript bundler (similar to tsup)
- TypeScript 5.9+: Strict mode enabled
- Biome: Linting and formatting
- c8: Code coverage
- VitePress: Documentation
Outputs:
dist/index.cjs- CommonJSdist/index.mjs- ES Moduledist/index.bundle.js- UMD for browsers (production)dev/index.cjs/dev/index.mjs- Debug builds with__ACI_DEV__ = true
npm run build # Production build
npm run lint # Check code quality
npm run lint-fix # Auto-fix issues
npm run format # Format code
npm test # Run tests with coverage
npm run test:types # Type check
npm run docs:dev # Serve docs locally
npm run docs:build # Build static docsProduction build:
__ACI_DEV__constant isfalse- Debug code removed by tree-shaking
- Minified for size
- All debug logs stripped
Development build:
__ACI_DEV__constant istrue- Includes
axios.debug()calls - Comprehensive logging
- Slightly larger bundle
Usage:
// Production
import { setupCache } from 'axios-cache-interceptor';
// Development
import { setupCache } from 'axios-cache-interceptor/dev';- axios: >=1.0.0 (peer dependency)
- cache-parser: Parse Cache-Control headers
- fast-defer: Efficient deferred promises
- http-vary: Vary header parsing and comparison
- object-code: Fast 32-bit hashing
- try: Safe function execution
- cache-parser: Robust Cache-Control parsing (many edge cases)
- fast-defer: Better than manual Promise constructor
- http-vary: RFC-compliant Vary header handling
- object-code: Fast deterministic hashing for object keys
// BAD - mutates cache
const res = await axios.get('/api/user');
res.data.name = 'Modified';
// GOOD - use cloning storage
setupCache(axios, {
storage: buildMemoryStorage(true) // cloneData = true
});// BAD - cache predicate failed but developer expects caching
setupCache(axios, {
cachePredicate: { statusCheck: [200] }
});
// Server returns 201 - won't be cached!
// GOOD - include all success codes you expect
cachePredicate: {
statusCheck: (status) => status >= 200 && status < 300;
}// BAD - default 32-bit hash, collision risk
setupCache(axios, { storage: redisStorage });
// GOOD - use cryptographic hash
setupCache(axios, {
storage: redisStorage,
generateKey: buildKeyGenerator((req) => {
return createHash('sha256')
.update(JSON.stringify([req.url, req.method, req.params]))
.digest('hex');
})
});// BAD - ignores server's cache directives
setupCache(axios, { interpretHeader: false });
// Server sends Cache-Control: no-store - still cached!
// GOOD - disable only for specific requests that need it
axios.get('/api/data', {
cache: { interpretHeader: false, ttl: 60000 }
});// BAD - disabling vary can cause cache poisoning
setupCache(axios, { vary: false });
// User A sees user B's data!
// GOOD - keep vary enabled (default)
setupCache(axios, { vary: true });- Default
maxEntries: 1024may need tuning - Large response bodies consume memory quickly
- Consider persistent storage (localStorage, Redis) for browser/server
maxStaleAgeprevents stale entry accumulation
- Request deduplication eliminates redundant calls
- Stale-while-revalidate minimizes perceived latency
- ETag/Last-Modified saves bandwidth (304 responses)
cacheTakeoverprevents double caching (browser + library)
- Key generation: O(1) hash with object-code
- Cache lookup: O(1) with Map storage
- Vary comparison: O(n) where n = number of vary headers
- Header interpretation: O(1) string parsing
- Axios version changed: 0.x → 1.x
- Breaking type changes
- New per-request
enabledflag - Deprecated
cache: falsein favor ofcache: { enabled: false }
These work but will be removed in v2:
locationoptionwaitingMap (exposed property)headerInterpreter(replacing with internal)requestInterceptor/responseInterceptor(replacing with internal)
Check:
- Is method in
cache.methods? (default: ['get', 'head']) - Does status code pass
cachePredicate.statusCheck? - Are headers saying "don't cache"? (enable debug mode)
- Is URL in
ignoreUrls? - Is
cache.enabledfalse?
Check:
- TTL too long?
interpretHeaderreading old Cache-Control max-age?- Clock skew causing Expires calculation issues?
override: falsewhen you need fresh data?
Check:
cleanupIntervaldisabled?maxEntriestoo high or disabled?- Stale entries accumulating (
maxStaleAgetoo high)? - Custom storage not implementing cleanup?
Check:
vary: falsewhen server uses Vary header?- Custom key generator not including all discriminating factors?
- Mutating cached data without
cloneData?
Check:
- Are request IDs identical? (check with debug mode)
- Different
cache.overridevalues? - One has
cache: false?
- Documentation: https://axios-cache-interceptor.js.org
- GitHub: https://github.com/arthurfiorette/axios-cache-interceptor
- Issues: https://github.com/arthurfiorette/axios-cache-interceptor/issues
- NPM: https://www.npmjs.com/package/axios-cache-interceptor
When working on this codebase:
- Read tests first - They document behavior better than comments
- Enable debug mode - Essential for understanding flow
- Test concurrent scenarios - Many bugs only appear with Promise.all
- Consider HTTP specs - RFC 7231, 7232, 7234 define expected behavior
- Update types - TypeScript is first-class citizen
- Document in VitePress - User-facing changes need docs
- Check deprecation warnings - Some APIs being removed in v2
- Avoid breaking changes - Library is stable, breaking changes need major version
import type {
AxiosCacheInstance,
CacheAxiosResponse,
CacheRequestConfig,
CacheProperties,
StorageValue,
AxiosStorage,
KeyGenerator,
HeaderInterpreter
} from 'axios-cache-interceptor';Setup:
import { setupCache } from 'axios-cache-interceptor';
const axios = setupCache(Axios.create());Cache check:
const cache = await axios.storage.get(requestId);
console.log(cache.state); // 'empty' | 'cached' | 'stale' | 'loading'Clear cache:
await axios.storage.remove(requestId);Inspect request:
const response = await axios.get('/api/data');
console.log(response.cached, response.stale, response.id);This guide should help you understand and work effectively with the axios-cache-interceptor codebase. For the most up-to-date information, always refer to the source code and tests.