How to use HttpClient and HttpInterceptor to Cache Requests in Angular 5

Recently, I’ve delved back into Angular on a new project for the first time since the Angular 1.x days. The new Angular seems familiar yet much more robust and productive since embracing object-oriented programming principles.

One requirement I had was to implement a simple request cache as data from the API I am consuming doesn’t change too frequently. I initially approached the problem with a simple solution but soon found some documentation related to the relatively new HttpClient API. HttpClient also supports registering an interceptor – a similar pattern to middleware in a modern webserver – via the HttpInterceptor interface. And to my delight, they even included a caching example using the aforementioned tools.

Since the example provided left some bits to the imagination, I’ve included a complete example in this guide.

First, we have our subclass of HttpInterceptor called CacheInterceptor.

cache.interceptor.ts

import { Injectable } from "@angular/core";
import { HttpEvent, HttpResponse } from "@angular/common/http";
import { RequestCacheService } from "../services/requestCache.service";

import { Observable } from "rxjs/Rx";

import {
  HttpRequest,
  HttpHandler,
  HttpInterceptor
} from "@angular/common/http";

const TTL = 10;

@Injectable()
export class CacheInterceptor implements HttpInterceptor {
  constructor(private cache: RequestCacheService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const cachedResponse = this.cache.get(req.url);
    return cachedResponse
      ? Observable.of(cachedResponse)
      : this.sendRequest(req, next);
  }

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

Here, intercept is invoked any time a request is made. First, we try to retrieve a cached instance of the response from our cache (we’ll look at this in a moment) and then we either return the cached response or we pass along the request to executed by calling sendRequest. Then, once sendRequest is invoked, it calls next to execute the next operation (similar to the middleware pattern). Once the operation has completed we cache the response. In this example we are using a hard-coded cache time of 10 seconds for simplicity.

I can imagine it would be possible to create a subclass of HttpClient in order to facilitate setting different TTLs on a per-request basis as well but I won’t get into that here.

Ok, so what about the cache? The cache is provided by a separate service called requestCache.service.ts.

requestCache.service.ts

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

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

  get(key): HttpResponse<any> {
    const tuple = this.cache.get(key);
    if (!tuple) return null;

    const expires = tuple[0];
    const httpResponse = tuple[1];

    // Don't observe expired keys
    const now = new Date();
    if (expires && expires.getTime() < now.getTime()) {
      this.cache.delete(key);
      return null;
    }

    return httpResponse;
  }

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

The cache is fairly simple. We have two instance methods: get and set. The first takes a key and returns a value unless it’s expired. The second takes a key, value, and time-to-live – or ttl – and stores the data in a Map. We use an in-memory Map here for storage, but you could use anything you need here to store the data. LocalStorage, for instance, would be suitable for longer term storage.

With these two components, we have everything we need for a simple request caching interceptor in our Angular application.

Don’t forget to provide the components to the dependency injector tree so that the cache service is available and the interceptor is registered to intercept http requests. Providing an HttpInterceptor is fairly involved but there are good instructions on how to do that here.

For anyone investigating Angular HttpInterceptors for the first time, I hope this guide is helpful.

Comments

Matt posted on 2018-04-27
Nice! I wonder if there's a way to incorporate server-side cache into this, where it actually asks the server if it should re-fetch the data or not.
Nicholas Rempel from https://nrempel.com posted on 2018-04-28
@Matt, Good idea! A common method for this would be for the server to return cache expiry information in response headers – usually through Etag: https://en.wikipedia.org/wiki/HTTP_ETag Alternatively, the client could make a HEAD request (if the server supports it) to check to see if the resource has changed before making a new request. https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html - section 9.4
tamir kratz posted on 2018-04-30
you are not niticing betweem GET & POST which means you are caching a POST requests also
maurelius posted on 2018-08-02
Just wanted to say thanks for this. I used your caching technique in my Angular 6 project and it works great!