ng-http-caching
v22.5.0
Published
Cache for HTTP requests in Angular application.
Downloads
11,994
Maintainers
Readme
NgHttpCaching 
Cache for HTTP requests in Angular application.
Description
Sometime there is a need to cache the HTTP requests so that browser doesn’t have to hit server to fetch same data when same service is invoked serially or in parallel. NgHttpCaching intercept all request are made, try to retrieve a cached instance of the response and then return the cached response or send the request to the backend. Once the operation has completed cache the response.
See the stackblitz demo.
Rendering on the server too? NgSsrCaching is the server-side sibling of this library: this one keeps the responses your application asks for, that one keeps the page your server rendered from them.
Features
✅ HTTP caching ✅ Handles simultaneous/parallel requests ✅ Automatic garbage collector of cache ✅ More than 90% unit tested ✅ LocalStorage, SessionStorage, MemoryStorage and custom cache storage ✅ Check response headers cache-control and expires ✅ Automatic cache invalidation on mutations (POST, PUT, DELETE, PATCH) ✅ Stale-while-revalidate ✅ Cross-tab invalidation ✅ ETag and Last-Modified conditional revalidation ✅ Server side rendering (SSR) safe
Get Started
Step 1: install ng-http-caching
npm i ng-http-cachingStep 2: Provide NgHttpCaching into your bootstrapApplication, eg.:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { AppComponent } from './app.component';
import { provideNgHttpCaching } from 'ng-http-caching';
bootstrapApplication(AppComponent, {
providers: [
provideNgHttpCaching(),
provideHttpClient(withInterceptorsFromDi())
]
});if you want configure ng-http-caching, you can pass a configuration, eg.:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { AppComponent } from './app.component';
import { provideNgHttpCaching, NgHttpCachingConfig } from 'ng-http-caching';
// your config...
const ngHttpCachingConfig: NgHttpCachingConfig = {
lifetime: 1000 * 10 // cache expire after 10 seconds
};
bootstrapApplication(AppComponent, {
providers: [
provideNgHttpCaching(ngHttpCachingConfig),
provideHttpClient(withInterceptorsFromDi())
]
});Functional interceptor
If you already use functional interceptors, you can register ngHttpCachingInterceptor
into withInterceptors() instead of withInterceptorsFromDi(), and choose its position
in the chain:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideNgHttpCaching, ngHttpCachingInterceptor } from 'ng-http-caching';
import { AppComponent } from './app.component';
bootstrapApplication(AppComponent, {
providers: [
provideNgHttpCaching(),
provideHttpClient(withInterceptors([ngHttpCachingInterceptor]))
]
});Use one way or the other, not both: registered twice, the caching would run twice on every request.
Config
This is all the configuration interface, see below for the detail of each config.
// all configuration are optionally
export interface NgHttpCachingConfig {
version?: string;
lifetime?: number;
staleTime?: number;
conditionalRevalidation?: boolean;
maxSize?: number;
allowedMethod?: string[];
cacheStrategy?: NgHttpCachingStrategy;
checkResponseHeaders?: boolean;
slidingExpiration?: boolean;
store?:
| NgHttpCachingStorageInterface
| NgHttpCachingNgSimpleStateSentinel
| (() => NgHttpCachingStorageInterface);
isExpired?: (entry: NgHttpCachingEntry, req?: HttpRequest<any>) => boolean | undefined | void;
isStale?: (entry: NgHttpCachingEntry, req?: HttpRequest<any>) => boolean | undefined | void;
keepInFlight?: boolean | ((req: HttpRequest<any>) => boolean | undefined | void);
isValid?: (entry: NgHttpCachingEntry) => boolean | undefined | void;
isCacheable?: (req: HttpRequest<any>) => boolean | undefined | void;
getKey?: (req: HttpRequest<any>) => string | undefined | void;
clearCacheOnMutation?: NgHttpCachingMutationStrategy | boolean | ((req: HttpRequest<any>) => boolean | undefined | void);
mutationInvalidation?: NgHttpCachingInvalidation;
responseSerializer?: <T>(body: T) => T;
}version (string - default: VERSION.major)
Cache version. When you have a breaking change, change the version, and it'll delete the current cache automatically. The default value is Angular major version (eg. 13), in this way, the cache is invalidated on every Angular upgrade.
lifetime (number - default: 3.600.000)
Number of millisecond that a response is stored in the cache.
You can set specific "lifetime" for each request by add the header X-NG-HTTP-CACHING-LIFETIME (see example below).
maxSize (number - default: 0)
Maximum number of entries kept into the cache store. When the limit is exceeded, the least
recently used entries are evicted. 0 (the default) means no limit.
const ngHttpCachingConfig: NgHttpCachingConfig = {
maxSize: 100, // keep at most 100 responses
};The access time is tracked in memory by the service. For a persistent store
(localStorage, sessionStorage) the entries restored by a previous page load have no
known access time, so they are evicted by the time they were added until they are read again.
staleTime (number - default: undefined)
Number of millisecond a response stays fresh. Once it is older than staleTime, and until
lifetime expires it, the cached response is still served right away, and a request is
sent to the backend in background to refresh the entry for the next reader. This is the
stale-while-revalidate behaviour.
const ngHttpCachingConfig: NgHttpCachingConfig = {
staleTime: 1000 * 30, // after 30 seconds a hit also refreshes the entry in background
lifetime: 1000 * 60 * 5, // after 5 minutes the entry is dropped and refetched
};undefined (the default) disables it, so a response is fresh until it expires. 0 makes
every cached response stale, so every hit serves the cache and refreshes it.
Some details worth knowing:
- the subscriber always gets one response, the cached one. The refreshed one is not emitted to it, it only lands into the cache;
- if the refresh fails, the cached response is kept and nothing is thrown: the caller has already been served;
- parallel hits on the same stale entry send a single refresh;
- the freshness is measured from the moment the body came from the backend, so
slidingExpirationkeeps an entry into the cache but doesn't make it fresh again; - an expired entry is never stale: it is refetched, and the caller waits for it;
- during server side rendering nothing is ever stale, revalidating would only slow the render down.
A long lifetime with a short staleTime also gives you a form of stale-if-error: past
staleTime every read is served from the cache and refreshed in background, so while the
backend is down the last good response keeps being served until lifetime expires it.
const ngHttpCachingConfig: NgHttpCachingConfig = {
staleTime: 1000 * 30, // refresh in background after 30 seconds
lifetime: 1000 * 60 * 60, // but keep serving the last good response for an hour
};conditionalRevalidation (boolean - default: true)
When a stale entry is refreshed and the cached response carries an ETag or a
Last-Modified, the refresh is sent as a conditional request (If-None-Match,
If-Modified-Since). If the backend answers 304 Not Modified there is no body to
download: the entry we already have is confirmed, and both its clocks restart.
It does nothing when the response carries no validator, so on most APIs it costs nothing.
Set it to false to always refetch the full body.
const ngHttpCachingConfig: NgHttpCachingConfig = {
staleTime: 1000 * 30,
conditionalRevalidation: false, // always download the body again
};Two notes. Angular reports a 304 as an error, because it isn't a 2xx: it is handled
inside the library, the subscriber never sees it. And the headers of the 304 are not
merged into the stored response, only the freshness is.
keepInFlight (boolean | function - default: false)
By default a request is cancelled when its last subscriber goes away: a destroyed
component, a takeUntilDestroyed, a route change while the list is still loading. The
response never arrives, so it never reaches the cache, and going back pays the whole round
trip again.
With keepInFlight: true a cacheable request runs to the end anyway. Nothing is emitted to
the subscriber that left, the response only fills the cache for the next one.
const ngHttpCachingConfig: NgHttpCachingConfig = {
keepInFlight: true,
// or per request:
// keepInFlight: (req) => req.urlWithParams.includes('/api/slow-report'),
};It applies only to the cacheable requests, and the kept requests are cancelled when the
NgHttpCachingService is destroyed, so nothing is left running at the end of a server side
rendered request. Careful with allowedMethod: ['ALL']: an abandoned mutation would still
reach the backend and still invalidate the cache.
checkResponseHeaders (boolean - default false);
If true response headers cache-control and expires are respected.
Cache-Control: no-store/no-cache, and an Expires already in the past, keep the response
out of the cache; max-age drives the entry lifetime. The Age header is subtracted from
max-age, so a response a CDN or proxy has already been holding isn't kept for the full
max-age again — when Age is greater than or equal to max-age the response is already
stale and isn't cached at all.
slidingExpiration (boolean - default: false)
If true the lifetime restarts at every cache hit: an entry that keeps being read never
expires, and only an entry left unused for a whole lifetime does.
const ngHttpCachingConfig: NgHttpCachingConfig = {
lifetime: 1000 * 60 * 5, // 5 minutes without a read, then the entry is dropped
slidingExpiration: true,
};A deadline coming from the response headers (max-age, Expires, with
checkResponseHeaders) belongs to the server, so it isn't moved: those entries expire when
the server said, however often you read them.
Note that every cache hit writes back to the store: with a persistent store
(localStorage, sessionStorage) that means a serialization on every read.
allowedMethod (string[] - default: ['GET', 'HEAD'])
Array of allowed HTTP methods to cache.
You can allow multiple methods, eg.: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD'] or
allow all methods by: ['ALL']. If allowedMethod is an empty array ([]), no response are cached.
Warning! NgHttpCaching use the full url (url with query parameters) as unique key for the cached response,
this is correct for the GET request but is potentially wrong for other type of request (eg. POST, PUT).
You can set a different "key" by customizing the getKey config method (see getKey section).
cacheStrategy (enum NgHttpCachingStrategy - default: NgHttpCachingStrategy.ALLOW_ALL)
Set the cache strategy, possible strategies are:
NgHttpCachingStrategy.ALLOW_ALL: All request are cacheable if HTTP method is intoallowedMethod;NgHttpCachingStrategy.DISALLOW_ALL: Only the request withX-NG-HTTP-CACHING-ALLOW-CACHEheader are cacheable if HTTP method is intoallowedMethod;
store (class of NgHttpCachingStorageInterface - default: NgHttpCachingMemoryStorage)
Set the cache store. You can implement your custom store by implement the NgHttpCachingStorageInterface interface, eg.:
import { NgHttpCachingConfig, NgHttpCachingStorageInterface } from 'ng-http-caching';
class MyCustomStore implements NgHttpCachingStorageInterface {
// ... your logic
}
const ngHttpCachingConfig: NgHttpCachingConfig = {
store: new MyCustomStore(),
};the default store is withNgHttpCachingMemoryStorage, an in-memory cache store:
import { NgHttpCachingConfig, withNgHttpCachingMemoryStorage } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
store: withNgHttpCachingMemoryStorage(),
};there is also a withNgHttpCachingLocalStorage a cache store with persistence into localStorage:
import { NgHttpCachingConfig, withNgHttpCachingLocalStorage } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
store: withNgHttpCachingLocalStorage(),
};and a withNgHttpCachingSessionStorage a cache store with persistence into sessionStorage:
import { NgHttpCachingConfig, withNgHttpCachingSessionStorage } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
store: withNgHttpCachingSessionStorage(),
};Both are safe to use with server side rendering: when localStorage/sessionStorage isn't
reachable (SSR, prerendering, sandboxed iframe, storage disabled by the user) they
transparently fall back to an in-memory storage, so nothing is persisted and nothing throws.
When the storage is full
Web storage is small (around 5 MB) and the browser refuses the write when it fills up. By default the oldest entries are evicted, one at a time, until the new one fits, so a single big response doesn't cost you the whole cache. You can change it:
import { NgHttpCachingConfig, withNgHttpCachingLocalStorage } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
store: () =>
withNgHttpCachingLocalStorage({
// 'evict-oldest' (default) | 'clear' (drop the whole cache) | 'ignore' (skip the write)
onQuotaExceeded: 'evict-oldest',
// how many entries to evict before giving up on the write, default 10
maxQuotaRetry: 10,
// prefix of the keys written into the storage, default 'NgHttpCaching::'.
// Give a different one to each application sharing the same origin, otherwise
// they read, evict and clear each other entries.
keyPrefix: 'my-app::',
}),
};With slidingExpiration: true every read rewrites the entry, so evicting the oldest is a
real least recently used eviction. Without it, entries are evicted in the order they were
added.
Server side rendering: pass a factory, not an instance
store also accepts a factory. This matters with server side rendering: the config object
is created once, when its module is loaded, so a store instance put in there is shared
by every request the server renders — and with it the cached responses of every user.
A factory is invoked once per NgHttpCachingService, so each rendered request gets its own:
import { NgHttpCachingConfig, withNgHttpCachingMemoryStorage } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
// ✅ one store per request
store: () => withNgHttpCachingMemoryStorage(),
// ❌ one store shared by all the server side rendered requests
// store: withNgHttpCachingMemoryStorage(),
};In the browser there is a single user per process, so both forms behave the same.
there is also a withNgHttpCachingBroadcastStorage, an in-memory store that tells the other
tabs when an entry stops being good:
import { NgHttpCachingConfig, withNgHttpCachingBroadcastStorage } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
store: () => withNgHttpCachingBroadcastStorage(),
};With the plain memory store a mutation in one tab leaves all the other tabs serving the old
response for a whole lifetime. This one sends the invalidation over a BroadcastChannel,
so the others know. The persistent stores don't need it: localStorage and
sessionStorage are already shared by every tab of the origin.
Two things it does on purpose:
- only the order travels, never the response. Nothing has to be serializable, nothing weighs on the channel, and no data moves between tabs;
- the tab that receives the order doesn't drop its entry, it marks it invalidated: the
next read is served right away and refreshed in background. This way an eviction decided
by another tab (garbage collector,
maxSize) can never empty this one.
Pass { channel: 'my-app' } to keep two applications of the same origin apart. Outside of a
browser the channel isn't opened at all, so the requests rendered on the server, which share
one process, don't invalidate each other.
One case stays uncovered: a request already in flight when the order arrives still writes its response, so a tab can cache data from before the mutation. Locally this is prevented, but a store can't see it.
and a withNgHttpCachingNgSimpleState adapter for use ng-simple-state as the cache storage.
To use this adapter, you must install ng-simple-state in your project:
npm i ng-simple-stateThen you can use it like this:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideNgHttpCaching } from 'ng-http-caching';
import { withNgHttpCachingNgSimpleState } from 'ng-http-caching/ng-simple-state';
import { AppComponent } from './app.component';
bootstrapApplication(AppComponent, {
providers: [
provideNgHttpCaching({
store: withNgHttpCachingNgSimpleState(),
}),
provideHttpClient(withInterceptorsFromDi())
]
});isExpired (function - default see NgHttpCachingService.isExpired());
If this function return true the request is expired and a new request is send to backend, if return false isn't expired.
If the result is undefined, the normal behaviour is provided.
The second argument is the request currently being served, so you can compare it with the one
that filled the cache (entry.request), eg. to expire the entry when the body of the request
has changed. It is undefined when the check comes from the garbage collector, where there is
no request in flight.
Example of customization:
import { NgHttpCachingConfig, NgHttpCachingEntry } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
isExpired: (entry: NgHttpCachingEntry): boolean | undefined => {
// In this example a special API endpoint (/my-endpoint) send into the body response
// an expireAt property (an ISO date string). Only for this endpoint the expiration
// is provided by expireAt value.
// For all the other endpoint normal behaviour is provided.
if( entry.request.urlWithParams.indexOf('/my-endpoint') !== -1 ){
// return true when the entry is expired, so a new request is sent to the backend
return Date.parse(entry.response.body.expireAt) <= Date.now();
}
// by returning "undefined" normal "ng-http-caching" workflow is applied
return undefined;
},
};isStale (function - default see NgHttpCachingService.isStale());
If this function return true the cache entry is stale, so it is served as it is and
refreshed in background, if return false it isn't stale.
If the result is undefined, the normal behaviour (staleTime) is provided.
import { NgHttpCachingConfig, NgHttpCachingEntry } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
isStale: (entry: NgHttpCachingEntry): boolean | undefined => {
// only this endpoint is refreshed in background, the others follow `staleTime`
if (entry.request.urlWithParams.indexOf('/my-endpoint') !== -1) {
return true;
}
return undefined;
},
};isValid (function - default see NgHttpCachingService.isValid());
If this function return true the cache entry is valid and can be stored, if return false isn't valid.
If the result is undefined, the normal behaviour is provided.
Default behaviour is whether the status code falls in the 2xx range and response headers cache-control and expires allow cache.
Example of customization:
import { NgHttpCachingConfig, NgHttpCachingEntry } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
isValid: (entry: NgHttpCachingEntry): boolean | undefined => {
// In this example only response with status code 200 can be stored into the cache
return entry.response.status === 200;
},
};isCacheable (function - default see NgHttpCachingService.isCacheable());
If this function return true the request is cacheable, if return false isn't cacheable.
If the result is undefined, the normal behaviour is provided.
Example of customization:
import { NgHttpCachingConfig } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
isCacheable: (req: HttpRequest<any>): boolean | undefined => {
// In this example the /my-endpoint isn't cacheable.
// For all the other endpoint normal behaviour is provided.
if( req.urlWithParams.indexOf('/my-endpoint') !== -1 ){
return false;
}
// by returning "undefined" normal "ng-http-caching" workflow is applied
return undefined;
},
};getKey (function - default see NgHttpCachingService.getKey());
This function return the unique key (string) for store the response into the cache.
If the result is undefined, the normal behaviour is provided.
Example of customization:
import { NgHttpCachingConfig } from 'ng-http-caching';
import * as hash from 'object-hash'; // install object-hash with: npm i object-hash
const hashOptions = {
algorithm: 'md5',
encoding: 'hex'
};
const ngHttpCachingConfig: NgHttpCachingConfig = {
allowedMethod: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD'],
getKey: (req: HttpRequest<any>): string | undefined => {
// In this example the full request is hashed for provide an unique key for the cache.
// This is important if you want support method like POST or PUT.
return req.method + '@' + req.urlWithParams + '@' + hash(req.params, hashOptions) + '@' + hash(req.body, hashOptions);
},
};clearCacheOnMutation (enum NgHttpCachingMutationStrategy | boolean | Function - default: NgHttpCachingMutationStrategy.NONE)
Set the mutation strategy for automatically clear the cache when a mutation request (POST, PUT, DELETE, PATCH) is successful. Possible strategies are:
NgHttpCachingMutationStrategy.NONE(orfalse): No automatic invalidation.NgHttpCachingMutationStrategy.ALL(ortrue): Clears the entire cache store on any successful mutation.NgHttpCachingMutationStrategy.IDENTICAL: Clears entries with the same URL (ignoring method and query params).NgHttpCachingMutationStrategy.COLLECTION: Clears entries with the same URL AND its parent collection URL (eg.DELETE /api/users/24invalidate alsoGET /api/users).IDENTICALandCOLLECTIONmatch the URL of the cached request, not the cache key, so they keep working with a customgetKey(see thegetKeysection).Function: Custom logic:(req: HttpRequest<any>) => boolean. It is called only for mutation requests; returningtrueclears the entire cache store, returningfalse(orundefined) skips the invalidation for that request.A request already in flight when the mutation succeeds describes the state the mutation has just changed, so its response is delivered to its subscriber but is not stored in the cache: otherwise it would silently outlive the invalidation for a whole lifetime.
Example of customization:
import { NgHttpCachingConfig, NgHttpCachingMutationStrategy } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
// clear the entire cache only if the mutation (POST, PUT, DELETE, PATCH)
// is on a specific endpoint
clearCacheOnMutation: (req) => req.url.includes('/api/critical-data')
};mutationInvalidation (enum NgHttpCachingInvalidation - default: DELETE)
What clearCacheOnMutation does to the entries it invalidates:
NgHttpCachingInvalidation.DELETE(the default): they are removed, so the next request waits for the backend;NgHttpCachingInvalidation.STALE: they are kept and marked stale, so the next request is served with the old response right away, while the entry is refreshed in background.
import { NgHttpCachingConfig, NgHttpCachingMutationStrategy, NgHttpCachingInvalidation } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
clearCacheOnMutation: NgHttpCachingMutationStrategy.COLLECTION,
// after a mutation the user still sees the list, and it refreshes by itself
mutationInvalidation: NgHttpCachingInvalidation.STALE,
};An invalidated entry is stale even without staleTime. It becomes fresh again as soon as
the refresh lands; if the refresh fails, the entry stays invalidated, so the next read
tries again. This can also be set for a single request with the HttpContext.
responseSerializer (function - default: undefined)
By default a cache hit serves the very same body instance kept into the store, and in dev mode
that body is made immutable, so that a consumer can't silently change what every later cache hit
serves. If your code needs to mutate the response (eg. an interceptor or a decorator that adapts
the body into a model in place), you get a TypeError like Cannot assign to read only property
'status'.
Set responseSerializer to return a copy of the body: the store keeps its own private copy, and
every reader, the request that filled the cache included, gets a fresh mutable one.
import { NgHttpCachingConfig } from 'ng-http-caching';
const ngHttpCachingConfig: NgHttpCachingConfig = {
responseSerializer: (body) => structuredClone(body)
};structuredClone doesn't support functions and class instances: if your body contains them, use
your own copy function. The serializer can also be set for a single request with the
HttpContext (see the HttpContext section), and the per request one wins over this config.
Headers
NgHttpCaching use some custom headers for customize the caching behaviour.
The supported headers are exported from NgHttpCachingHeaders:
export const NgHttpCachingHeaders = {
ALLOW_CACHE: 'X-NG-HTTP-CACHING-ALLOW-CACHE',
DISALLOW_CACHE: 'X-NG-HTTP-CACHING-DISALLOW-CACHE',
LIFETIME: 'X-NG-HTTP-CACHING-LIFETIME',
TAG: 'X-NG-HTTP-CACHING-TAG',
};All those headers are removed before send the request to the backend.
X-NG-HTTP-CACHING-ALLOW-CACHE (string: any value);
If you have choose the DISALLOW_ALL cache strategy, you can mark specific request as cacheable by adding the header X-NG-HTTP-CACHING-ALLOW-CACHE, eg.:
this.http.get('https://my-json-server.typicode.com/typicode/demo/db', {
headers: {
[NgHttpCachingHeaders.ALLOW_CACHE]: '1',
}
}).subscribe(e => console.log(e));X-NG-HTTP-CACHING-DISALLOW-CACHE (string: any value);
You can disallow specific request by add the header X-NG-HTTP-CACHING-DISALLOW-CACHE, eg.:
this.http.get('https://my-json-server.typicode.com/typicode/demo/db', {
headers: {
[NgHttpCachingHeaders.DISALLOW_CACHE]: '1',
}
}).subscribe(e => console.log(e));X-NG-HTTP-CACHING-LIFETIME (string: number of millisecond);
You can set specific lifetime for request by add the header X-NG-HTTP-CACHING-LIFETIME with a string value as the number of millisecond, eg.:
0 means "never expire", while a blank value is ignored and the configured lifetime is used.
this.http.get('https://my-json-server.typicode.com/typicode/demo/db', {
headers: {
[NgHttpCachingHeaders.LIFETIME]: (1000 * 60 * 60 * 24 * 365).toString(), // one year
}
}).subscribe(e => console.log(e));X-NG-HTTP-CACHING-TAG (string: tag name);
You can tag multiple request by adding special header X-NG-HTTP-CACHING-TAG with the same tag and
using NgHttpCachingService.clearCacheByTag(tag: string) for delete all the tagged request. Eg.:
this.http.get('https://my-json-server.typicode.com/typicode/demo/db?id=1', {
headers: {
[NgHttpCachingHeaders.TAG]: 'foo',
}
}).subscribe(e => console.log(e));HttpContext
You can override NgHttpCachingConfig methods:
{
isExpired?: (entry: NgHttpCachingEntry, req?: HttpRequest<any>) => boolean | undefined | void;
isStale?: (entry: NgHttpCachingEntry, req?: HttpRequest<any>) => boolean | undefined | void;
keepInFlight?: boolean | ((req: HttpRequest<any>) => boolean | undefined | void);
isValid?: (entry: NgHttpCachingEntry) => boolean | undefined | void;
isCacheable?: (req: HttpRequest<any>) => boolean | undefined | void;
getKey?: (req: HttpRequest<any>) => string | undefined | void;
clearCacheOnMutation?: NgHttpCachingMutationStrategy | boolean | ((req: HttpRequest<any>) => boolean | undefined | void);
mutationInvalidation?: NgHttpCachingInvalidation;
responseSerializer?: <T>(body: T) => T;
}with HttpContextToken, eg.:
import { withNgHttpCachingContext } from 'ng-http-caching';
const context = withNgHttpCachingContext({
isExpired: (entry: NgHttpCachingEntry) => {
console.log('context:isExpired', entry);
},
isCacheable: (req: HttpRequest<any>) => {
console.log('context:isCacheable', req);
},
getKey: (req: HttpRequest<any>) => {
console.log('context:getKey', req);
},
isValid: (entry: NgHttpCachingEntry) => {
console.log('context:isValid', entry);
}
});
this.http.get('https://my-json-server.typicode.com/typicode/demo/db?id=1', { context }).subscribe(e => console.log(e));The context is read from the request being served, so the overrides keep working with a
persistent store (localStorage, sessionStorage) even though an HttpContext, holding
live functions, can't be serialized with the cache entry.
Cache service
You can inject into your component the NgHttpCachingService that expose some utils methods:
export class NgHttpCachingService {
/**
* Return the config
*/
getConfig(): Readonly<NgHttpCachingConfig>;
/**
* Return the queue map
*/
getQueue(): Readonly<Map<string, Observable<HttpEvent<any>>>>;
/**
* Return the cache store
*/
getStore(): Readonly<NgHttpCachingStorageInterface>;
/**
* Return response from cache
*/
getFromCache<K, T>(req: HttpRequest<K>): Readonly<HttpResponse<T>> | undefined;
/**
* Return the response from cache, together with whether it is stale
*/
getFromCacheWithState<K, T>(req: HttpRequest<K>): { response: Readonly<HttpResponse<T>>; stale: boolean } | undefined;
/**
* Add response to cache
*/
addToCache<K, T>(req: HttpRequest<K>, res: HttpResponse<T>): boolean;
/**
* Delete response from cache
*/
deleteFromCache<K>(req: HttpRequest<K>): boolean;
/**
* Clear the cache
*/
clearCache(): void;
/**
* Clear the cache by key
*/
clearCacheByKey(key: string): boolean;
/**
* Clear the cache by keys
*/
clearCacheByKeys(keys: Array<string>): number;
/**
* Clear the cache by regex
*/
clearCacheByRegex<K, T>(regex: RegExp): number;
/**
* Clear the cache by TAG
*/
clearCacheByTag<K, T>(tag: string): number;
/**
* Mark every cache entry as invalidated: they are still served, but stale, so the
* first read of each refreshes it in background
*/
invalidateCache(): number;
/**
* Mark the cache entry for the provided key as invalidated
*/
invalidateCacheByKey(key: string): boolean;
/**
* Mark the cache entries for the provided keys as invalidated
*/
invalidateCacheByKeys(keys: string[]): number;
/**
* Mark the cache entries whose key match the regex as invalidated
*/
invalidateCacheByRegex(regex: RegExp): number;
/**
* Mark the cache entries having the provided TAG as invalidated
*/
invalidateCacheByTag(tag: string): number;
/**
* Clear the cache according to the `clearCacheOnMutation` strategy.
* Called automatically by the interceptor on every successful mutation.
*/
clearCacheByMutation<K>(req: HttpRequest<K>): boolean;
/**
* Run garbage collector (delete expired cache entry)
*/
runGc<K, T>(): boolean;
/**
* Return true if cache entry is expired
*/
isExpired<K, T>(entry: NgHttpCachingEntry<K, T>, req?: HttpRequest<K>): boolean;
/**
* Return the request to send to refresh a stale entry, with the conditional headers
* when the cached response carries a validator
*/
getConditionalRequest<K, T>(req: HttpRequest<K>): HttpRequest<K>;
/**
* Confirm the cached entry after a 304 Not Modified: both clocks restart
*/
confirmFromCache<K, T>(req: HttpRequest<K>): Readonly<HttpResponse<T>> | undefined;
/**
* Return true if the cache entry is stale, so it is served as it is and refreshed
* in background
*/
isStale<K, T>(entry: NgHttpCachingEntry<K, T>, req?: HttpRequest<K>): boolean;
/**
* Return true if cache entry is valid for store in the cache
* Default behaviour is whether the status code falls in the 2xx range and response headers cache-control and expires allow cache.
*/
isValid<K, T>(entry: NgHttpCachingEntry<K, T>): boolean;
/**
* Return true if the request is cacheable
*/
isCacheable<K>(req: HttpRequest<K>): boolean;
/**
* Return the cache key.
* Default key is http method plus url with query parameters, eg.:
* `GET@https://github.com/nigrosimone/ng-http-caching`
*/
getKey<K>(req: HttpRequest<K>): string;
/**
* Return observable from cache
*/
getFromQueue<K, T>(req: HttpRequest<K>): Observable<HttpEvent<T>> | undefined;
/**
* Add observable to cache
*/
addToQueue<K, T>(req: HttpRequest<K>, obs: Observable<HttpEvent<T>>): void;
/**
* Delete observable from cache
*/
deleteFromQueue<K>(req: HttpRequest<K>): boolean;
}Examples
Below there are some examples of use case.
Example: exclude specific request from cache
You can disallow specific request by add the header X-NG-HTTP-CACHING-DISALLOW-CACHE, eg.:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { NgHttpCachingHeaders } from 'ng-http-caching';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
private readonly http = inject(HttpClient);
constructor() {
// This request will never cache.
// Note: all the "special" headers in NgHttpCachingHeaders are removed before send the request to the backend.
this.http.get('https://my-json-server.typicode.com/typicode/demo/db', {
headers: {
[NgHttpCachingHeaders.DISALLOW_CACHE]: '1',
}
}).subscribe(e => console.log(e));
}
}Example: set specific lifetime for request
You can set specific lifetime for request by add the header X-NG-HTTP-CACHING-LIFETIME with a string value as the number of millisecond, eg.:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { NgHttpCachingHeaders } from 'ng-http-caching';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
private readonly http = inject(HttpClient);
constructor() {
// This request will expire from 365 days.
// Note: all the "special" headers in NgHttpCachingHeaders are removed before send the request to the backend.
this.http.get('https://my-json-server.typicode.com/typicode/demo/db', {
headers: {
[NgHttpCachingHeaders.LIFETIME]: (1000 * 60 * 60 * 24 * 365).toString(),
}
}).subscribe(e => console.log(e));
}
}Example: mark specific request as cacheable (if cache strategy is DISALLOW_ALL)
If you have choose the DISALLOW_ALL cache strategy, you can mark specific request as cacheable by adding the header X-NG-HTTP-CACHING-ALLOW-CACHE, eg.:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { NgHttpCachingHeaders } from 'ng-http-caching';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
private readonly http = inject(HttpClient);
constructor() {
// This request is marked as cacheable (this is necessary only if cache strategy is DISALLOW_ALL)
// Note: all the "special" headers in NgHttpCachingHeaders are removed before send the request to the backend.
this.http.get('https://my-json-server.typicode.com/typicode/demo/db', {
headers: {
[NgHttpCachingHeaders.ALLOW_CACHE]: '1',
}
}).subscribe(e => console.log(e));
}
}Example: clear/flush all the cache
If user switch the account (logout/login) or the application language, maybe ca be necessary clear all the cache, eg.:
import { Component, inject } from '@angular/core';
import { NgHttpCachingService } from 'ng-http-caching';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
private readonly ngHttpCachingService = inject(NgHttpCachingService);
clearCache(): void {
// Clear all the cache
this.ngHttpCachingService.clearCache();
}
}Example: clear/flush specific cache entry
If you want delete some cache entry, eg.:
import { Component, inject } from '@angular/core';
import { NgHttpCachingService } from 'ng-http-caching';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
private readonly ngHttpCachingService = inject(NgHttpCachingService);
clearCache(key: string): boolean {
// Clear the cache for the provided key
return this.ngHttpCachingService.clearCacheByKey(key);
}
}Example: clear/flush specific cache entry by RegEx
If you want delete some cache entry by RegEx, eg.:
import { Component, inject } from '@angular/core';
import { NgHttpCachingService } from 'ng-http-caching';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
private readonly ngHttpCachingService = inject(NgHttpCachingService);
clearCacheByRegex(regEx: RegExp): void {
// Clear the cache for the key that match regex
this.ngHttpCachingService.clearCacheByRegex(regEx);
}
}Example: TAG request and clear/flush specific cache entry by TAG
You can tag multiple request by adding special header X-NG-HTTP-CACHING-TAG with the same tag and
using NgHttpCachingService.clearCacheByTag(tag: string) for delete all the tagged request. Eg.:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { NgHttpCachingService, NgHttpCachingHeaders } from 'ng-http-caching';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
private readonly ngHttpCachingService = inject(NgHttpCachingService);
private readonly http = inject(HttpClient);
constructor() {
// This request is tagged with "foo" keyword. You can tag multiple requests with the same tag and
// using NgHttpCachingService.clearCacheByTag("foo") for delete all the tagged request.
this.http.get('https://my-json-server.typicode.com/typicode/demo/db?id=1', {
headers: {
[NgHttpCachingHeaders.TAG]: 'foo',
}
}).subscribe(e => console.log(e));
// This request is also tagged with "foo" keyword, and has another tag "baz".
// You can add multiple tags comma separated.
this.http.get('https://my-json-server.typicode.com/typicode/demo/db?id=2', {
headers: {
[NgHttpCachingHeaders.TAG]: 'foo,baz',
}
}).subscribe(e => console.log(e));
}
clearCacheForFoo(): void {
// Clear the cache for all the entry have the tag 'foo'
this.ngHttpCachingService.clearCacheByTag('foo');
}
}Limitations
NgHttpCaching is a TTL cache with request deduplication, not a full implementation of
the HTTP caching semantics. Know what it doesn't do before you rely on it.
⚠️ The cache key ignores the request headers
The default key is method@url (see the getKey section):
two requests to the same URL share the same cache entry even if they carry different
headers. For an Authorization header this means that, after a user switch, the previous
user's response can still be served from the cache.
If you cache anything user specific, do one of the following:
clear the cache when the identity changes — the simplest and safest option:
// on login, logout, tenant or language switch this.ngHttpCachingService.clearCache();or make the identity part of the key:
const ngHttpCachingConfig: NgHttpCachingConfig = { getKey: (req) => req.method + '@' + req.urlWithParams + '@' + (req.headers.get('Authorization') ?? ''), };or keep user specific requests out of the cache entirely, with the
X-NG-HTTP-CACHING-DISALLOW-CACHEheader or a customisCacheable.
Vary is not supported
The Vary response header is ignored. A response negotiated on Accept-Language,
Accept-Encoding or any other header is served to every request matching the cache key,
whatever the negotiation was. Use getKey to include the relevant headers yourself.
Conditional revalidation only on a stale entry
ETag and Last-Modified are used when a stale entry is refreshed (see
conditionalRevalidation). An entry past its lifetime is dropped and refetched in full
instead, because it isn't in the cache anymore when the request goes out. Likewise
no-cache is treated as "don't store" rather than "store, but revalidate before use", and
the headers of a 304 are not merged into the stored response.
Alternatives
Aren't you satisfied? there are some valid alternatives:
- @ngneat/cashew: the closest one, an interceptor with per request options, buckets and a persistent store.
- ts-cacheable (ex
ngx-cacheable): not an interceptor,@Cacheable/@CacheBusterdecorators on your service methods. - @tanstack/angular-query: not a cache for
HttpClientbut a full query library, with background refetch and stale-while-revalidate. - p3x-angular-http-cache-interceptor: minimal, no lifetime and no API to clear the cache.
Support
This is an open-source project. Star this repository, if you like it, or even donate. Thank you so much!
My other libraries
I have published some other Angular libraries, take a look:
- NgSimpleState: Simple state management in Angular with only Services and RxJS or Signal
- NgGenericPipe: Generic pipe for Angular application for use a component method into component template.
- NgLet: Structural directive for sharing data as local variable into html component template
- NgForTrackByProperty: Angular global trackBy property directive with strict type checking
- NgSsrCaching: Cache for server-side rendered pages in Angular SSR
