Angular Http Caching using Interceptor and Refresh

This article is written assuming you have a fair understanding of how an Angular Interceptor works and what they do.

Caching an Http request for GET calls is highly important as it will avoid making an API service calls unnecessarily.

An Interceptor can be used to delegate caching without disturbing your existing Http calls. Angular documentation already outlines on these subjects and can be found here (https://angular.io/guide/http#caching-requests), but it is missing the full implementation.

Let’s dig deeper and implement our own in-memory Angular Http caching mechanism. For this we need a CacheResolverService and a CacheInterceptor.

//
// cache-resolver.service.ts
//

import { HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';

@Injectable()
export class CacheResolverService {
  private cache = new Map<string, [Date, HttpResponse<any>]>();

  constructor() {}

  set(key, value, timeToLive: number | null = null) {
    console.log('Set cache key', key);

    if (timeToLive) {
      const expiresIn = new Date();
      expiresIn.setSeconds(expiresIn.getSeconds() + timeToLive);
      this.cache.set(key, [expiresIn, value]);
    } else {
      this.cache.set(key, [null, value]);
    }
  }
}


//

A CacheResolverService is a simple dependency injectable service that sets and gets URL query strings along with HttpResponse.

The private cache is instantiated with a Map class that has a data type of “string” and a tuple of “Date” and HttpResponse Object or collections.

We will store HttpResponse in a map with “key” as a unique identifier.

KeyValue
/api/v1/users?name=jer&username=jer(2) [Tue Mar 29 2022 13:35:58 GMT+0530 (India Standard Time), HttpResponse]
/api/v1/users?name=vin&username=vin(2) [Tue Mar 29 2022 13:39:23 GMT+0530 (India Standard Time), HttpResponse]
Example: Cached using Map with Key as unique identifier.

Implementing a get cache is fairly easy, Map has a builtin method “Map().get(key)” to fetch the stored values using identifier key.

//
// cache-resolver.service.ts
//

get(key) {
  const tuple = this.cache.get(key);

  if(!tuple) return null;

  // Extract tuple
  const expiresIn = tuple[0];
  const httpSavedResponse = tuple[1];
  const now = new Date();

  // Check if Time To Live has expired
  if(expiresIn && expiresIn.getTime() < now.getTime()) {
    // Delete if expired
    this.cache.delete(key);
    return null;
  }

  return httpSavedResponse;
}

//

The get logic is fairly simple;

  1. Using key as an argument, we are fetching the stored values in Mapped cache.
  2. If the cache is null then a null is returned to the CacheInterceptor.
  3. Otherwise, the cache is extracted in two parts, 1) is expiresIn date in seconds and 2) The actual stored Http response.
  4. If time to live is expired then delete existing values using key and return null to CacheInterceptor.
  5. Otherwise send the cached httpSavedResponse.

CacheInterceptor

import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest,
  HttpResponse,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { of } from 'rxjs/internal/observable/of';
import { tap } from 'rxjs/internal/operators/tap';
import { CacheResolverService } from '../services/cache-resolver.service';

const TIME_TO_LIVE = 10;

@Injectable()
export class CacheInterceptor implements HttpInterceptor {
  constructor(private cacheResolver: CacheResolverService) {}
  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {

    // Pass through if it's not GET call
    if (req.method !== 'GET') {
      return next.handle(req);
    }

    // Check if latestCheckbox is ticked to get new results
    // Catch-then-refresh
    if (req.headers.get('x-refresh')) {
      return this.sendRequest(req, next);
    }
    const cachedResponse = this.cacheResolver.get(req.url);

    return cachedResponse ? of(cachedResponse) : this.sendRequest(req, next);
  }

  sendRequest(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      tap((event) => {
        if (event instanceof HttpResponse) {
          this.cacheResolver.set(req.url, event, TIME_TO_LIVE);
        }
      })
    );
  }
}

Firstly, a cache interceptor is just like any other interceptors. Our cache interceptor initializes the cache resolver in the constructor to make use of the caching mechanism.

ALSO READ  Angular 8 / 9 - Change Detection Strategy - OnPush, Default - Use Case Scenario

Next, we need to capture only the GET method calls from the request, and let other methods pass through, since functionally we have to cache only the API’s of GET calls alone.

There is a special header validation called “x-refresh”, this header can be set from data service classes to bypass caching mechanisms all together. This could be a feature based on your application requirements.

The last part is where we make use of the CacheResolverService.get(key) method to fetch stored values.

If there are values, then the cached values are projected using the “of” Observable operator. Otherwise a fresh call is made.

In this simple demo in stackblitz you can see how the cache mechanism comes in to play

In Conclusion

This article is written to outline the basic principle of Http caching technique. Our caching mechanism is bare minimum and simple, however if you refresh the browser tab, the stored values will be removed. We can extend the cache mechanism by saving them in localStorage and making them persistent. For that a separate service class just to handle storage has to be created.