Recently, I’ve delved back into Angular on a new project for the first time since the Angular 1 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.

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 pass along the request to execute 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 been completed, we cache the response. In this example, we use 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.

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 can 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.