HTTP response caching in Angular

Reducing the HTTP request and response traffic between the client and the server can help to boost an application's performance. A reduction in the time it takes to fetch data from the server results in faster page loading times, leading to improved user-experience.

Angular interceptors are a form of middleware, which sit between the HTTP client and the server, they can be used to effect various tasks including caching. In the following use-case the interceptor function is used to cache responses, in order to reduce server request traffic.

See my demo application of HTTP response caching with unit and e2e tests at GitHub

Defining the HTTP interceptor function

The interceptor function takes two arguments, the HTTP request and the next handler function. It will be called by every HTTP request:

caching-interceptor-fn.ts

export function cachingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent> {
  if (req.method === 'GET') {
  }
  else {
  }
}

Providing the interceptor function to the application

The interceptor function is declared in the application root providers array, using provideHttpClient and the withInterceptors feature:

app.config.ts

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([
        cachingInterceptor
      ])
    )
  ]
};

Cached responses storage management

CacheStoreService manages the storage of cached responses, it initialises a Map() object to hold key-value pairs where the key is the request URL and the value is an array of the response and a timestamp. Methods are defined to check, populate and clear responses:

cache-store.service.ts

@Injectable()
export class CacheStoreService {
  public cachedResponses = new Map<string, HttpResponse<unknown>>();
  getResponse(requestUrl: string): [HttpResponse<unknown>, Date] | undefined {
    return this.cachedResponses.get(requestUrl);
  }
  addResponse(requestUrl: string, response: HttpResponse<unknown>, timestamp: Date): void {
    this.cachedResponses.set(requestUrl, [response, timestamp]);
  }
  clearResponse(requestUrl: string): void {
    this.cachedResponses.delete(requestUrl);
  }
}

Keeping the cached responses up-to-date

To keep the cached responses up-to-date they are cleared after a period of 60 minutes has elapsed. The setTimestamp function returns the current date and time, which is saved as the timestamp for the cached response. The isExpired function calculates the time elapsed from the timestamp argument, the duration and the current time, it returns true if the response has expired otherwise it returns false:

cache-timer.service.ts

@Injectable()
export class CacheTimerService {
  durationInMins = 60;
  setTimestamp(): Date {
    return new Date();
  }
  isExpired(timestamp: Date): boolean {
    const timeNow = new Date().getTime();
    const duration = this.duration * 60 * 1000;
    const expiryTime = timestamp.getTime() + durationInMillis;
    return (expiryTime > timeNow) ? false : true;
  }
}

The response caching functionality

  • The cacheStore and cacheTimer services are injected into the interceptor to manage the storage and lifespan of cached responses.
  • The request is checked to determine if it can be cached, only GET requests can be cached, others are returned as an observable.
  • A cached response for the request is searched, if found its expiry is checked, if the cached response has not expired it is returned.
  • If there is no cached response for the request or its cached response has expired, it is forwarded to the next function for processing.
  • The request is sent to the server, the returned response, the request URL and a timestamp are added to cache using the tap operator.

caching-interceptor-fn.ts

export function cachingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn):
  Observable<HttpEvent<unknown>> {

  const cacheStoreService = inject(CacheStoreService);
  const cacheTimerService = inject(CacheTimerService);

  if (req.method === 'GET') {
    const cachedResponse = cacheStoreService.getResponse(req.url);
    if (cachedResponse) {
      if (!cacheTimerService.isExpired(cachedResponse[1])) {
        return of(cachedResponse[0]);
      }
      else {
        cacheStoreService.clearResponse(req.url);
      }
    }
    return next(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          cacheStoreService.addResponse(req.url, event, cacheTimerService.setTimestamp());
        }
      })
    );
  }
  else {
    return next(req);
  }
}