Angular Caching Http Interceptor
July 04, 2018
This page will walk through Angular caching Http Interceptor example. Angular provides HttpInterceptor
interface that is used to intercept HttpRequest
and handle them. HttpInterceptor
has a intercept()
method. To create an Interceptor, we need to create a service by implementing HttpInterceptor
interface and overriding its intercept()
method. There can be more than one interceptor in our applications and these interceptors run in the given order. Interceptors transform the outgoing request before passing it to the next interceptor in the chain. Interceptor passes the request to the next Interceptor by calling handle()
method of HttpHandler
.
Here on this page we will create Http Interceptor to cache the response. The cache Interceptor first checks if the request is cachable or not. If request is not cachable then response is generated by running the request URL. If request is cachable then we fetch cached response and if it is null then we run the request URL to generate the response and it is also added to cache. We will expire the cache after fixed time. To store the cache we will use
Map
in our example. Now find the complete example step by step.
Contents
Technologies Used
Find the technologies being used in our example.1. Angular 6.0.3
2. Angular CLI 6.0.3
3. TypeScript 2.7.2
4. Node.js 10.3.0
5. NPM 6.1.0
6. In-Memory Web API 0.6.0
Project Structure
Find the project structure of our demo application.my-app | |--src | | | |--app | | | | | |--http-interceptors | | | | | | | |--index.ts | | | |--logging-interceptor.ts | | | |--caching-interceptor.ts | | | | | |--services | | | | | | | |--cache.ts | | | |--cache-entry.ts | | | |--cache-map.service.ts | | | |--book.service.ts | | | | | |--book.ts | | |--book.component.ts | | |--book.component.html | | | | | |--test-data.ts | | | | | |--app.component.ts | | |--app.module.ts | | | |--main.ts | |--index.html | |--styles.css | |--node_modules |--package.json
Create Cache Service
We will create a cache service to get and put response into cache. For caching a response we are usingMap
. First of all we will create an abstract class to define caching operation.
cache.ts
import { HttpRequest, HttpResponse } from '@angular/common/http'; export abstract class Cache { abstract get(req: HttpRequest<any>): HttpResponse<any> | null; abstract put(req: HttpRequest<any>, res: HttpResponse<any>): void; }
get()
method to fetch response from cache and put()
method to put response into cache.
In our example, a cache entry will have following properties.
cache-entry.ts
import { HttpResponse } from '@angular/common/http'; export interface CacheEntry { url: string; response: HttpResponse<any> entryTime: number; } export const MAX_CACHE_AGE = 20000; // in milliseconds
response: Response to be cached as
HttpResponse
.
entryTime: Time when response is cached. It will help to find out expired cache.
MAX_CACHE_AGE
decides the age of cache. After this time, cache will be considered as expired.
Now we will create our service to get and put cache using
Map
.
cache-map.service.ts
import { Injectable } from '@angular/core'; import { HttpRequest, HttpResponse } from '@angular/common/http'; import { Cache } from './cache'; import { CacheEntry, MAX_CACHE_AGE } from './cache-entry'; @Injectable() export class CacheMapService implements Cache { cacheMap = new Map<string, CacheEntry>(); get(req: HttpRequest<any>): HttpResponse<any> | null { const entry = this.cacheMap.get(req.urlWithParams); if (!entry) { return null; } const isExpired = (Date.now() - entry.entryTime) > MAX_CACHE_AGE; return isExpired ? null : entry.response; } put(req: HttpRequest<any>, res: HttpResponse<any>): void { const entry: CacheEntry = { url: req.urlWithParams, response: res, entryTime: Date.now() }; this.cacheMap.set(req.urlWithParams, entry); this.deleteExpiredCache(); } private deleteExpiredCache() { this.cacheMap.forEach(entry => { if ((Date.now() - entry.entryTime) > MAX_CACHE_AGE) { this.cacheMap.delete(entry.url); } }) } }
1. In
get()
method, we are fetching cached response from our Map
for a request URL. If the result is undefined, it means there is no cached response for that URL, then we are returning null. If there is cached response for that URL, then first we check if it is expired or not. If cache is expired then we return null. If cache is not expired then return cached response.
2. In
put()
method, first we are creating cache entry with request URL, response and entry time. Then we set this cache entry into the Map
. We also delete all expired cache.
Now configure the cache service in provider as following.
@NgModule({ providers: [ CacheMapService, { provide: Cache, useClass: CacheMapService } ], ------ }) export class AppModule { }
Create Cache Interceptor
To create an Http interceptor we need to create a service that will implementHttpInterceptor
interface and implement its intercept()
method.
caching-interceptor.ts
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpResponse, HttpHandler } from '@angular/common/http'; import { of } from 'rxjs'; import { tap } from 'rxjs/operators'; import { CacheMapService } from '../services/cache-map.service'; const CACHABLE_URL = "/api/booksSearch"; @Injectable() export class CachingInterceptor implements HttpInterceptor { constructor(private cache: CacheMapService) {} intercept(req: HttpRequest<any>, next: HttpHandler) { if (!this.isRequestCachable(req)) { return next.handle(req); } const cachedResponse = this.cache.get(req); if (cachedResponse !== null) { return of(cachedResponse); } return next.handle(req).pipe( tap(event => { if (event instanceof HttpResponse) { this.cache.put(req, event); } }) ); } private isRequestCachable(req: HttpRequest<any>) { return (req.method === 'GET') && (req.url.indexOf(CACHABLE_URL) > -1); } }
method
, urlWithParams
etc.
HttpHandler: It transforms an
HttpRequest
into a stream of HttpEvent
. HttpHandler
provides handle()
method to dispatch request from first interceptor to second and so on.
HttpResponse: It is a full HTTP response. It has getter methods such as
body
, headers
, status
, url
etc.
In our cache Interceptor, we are processing following steps.
1. First we will check if request is cachable, if not then we pass request for next processing using
handle()
method of HttpHandler
.
2. If request is cachable, fetch it from cache and if it is not null then return the response as
Observable
.
3. If cached response is null then let the request processed and then cache its response and return it.
Now we will configure our interceptor service into
providers
of @NgModule
. In our example we have logging interceptor and cache interceptor. Interceptors are executed in the order we provide. We have created const
array of our interceptors.
export const httpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true } ];
providers
attribute.
@NgModule({ providers: [ httpInterceptorProviders ], ------ }) export class AppModule { }
Other Components, Services and Module used in Example
index.tsimport { HTTP_INTERCEPTORS } from '@angular/common/http'; import { LoggingInterceptor } from './logging-interceptor'; import { CachingInterceptor } from './caching-interceptor'; export const httpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true } ];
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http'; import { finalize, tap } from 'rxjs/operators'; @Injectable() export class LoggingInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler) { const startTime = Date.now(); let status: string; return next.handle(req).pipe( tap( event => { status = ''; if (event instanceof HttpResponse) { status = 'succeeded'; } }, error => status = 'failed' ), finalize(() => { const elapsedTime = Date.now() - startTime; const message = req.method + " " + req.urlWithParams +" "+ status + " in " + elapsedTime + "ms"; this.logDetails(message); }) ); } private logDetails(msg: string) { console.log(msg); } }
export interface Book { id: number; name: string; category: string; year: string; }
import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { Book } from '../book'; @Injectable() export class BookService { constructor(private http: HttpClient) { } bookUrl = "/api/booksSearch"; searchBooksByTitle(searchQuery: string): Observable<Book[]> { let httpParams = new HttpParams() .set('name', searchQuery) return this.http.get<Book[]>(this.bookUrl, { params: httpParams }); } }
import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { BookService } from './services/book.service'; import { Book } from './book'; @Component({ selector: 'app-book', templateUrl: './book.component.html' }) export class BookComponent implements OnInit { books$: Observable<Book[]> searchQuery: string; constructor(private bookService: BookService) { } ngOnInit() { } searchBooks() { this.books$ = this.bookService.searchBooksByTitle(this.searchQuery); } }
<input [(ngModel)] = "searchQuery"> <button (click)="searchBooks()">Search</button><br/> <ul> <li *ngFor="let book of books$ | async" > Id: {{book.id}}, Name: {{book.name}}, Category: {{book.category}}, Year: {{book.year}} </li> </ul>
import { InMemoryDbService } from 'angular-in-memory-web-api'; export class TestData implements InMemoryDbService { createDb() { let bookDetails = [ { id: '101', name: 'Angular by Krishna', category: 'Angular', year: '2015' }, { id: '102', name: 'Spring by Rama', category: 'Spring', year: '2016' } ]; return { booksSearch: bookDetails }; } }
import { Component } from '@angular/core'; @Component({ selector: 'app-root', template: ` <app-book></app-book> ` }) export class AppComponent { }
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { AppComponent } from './app.component'; import { BookComponent } from './book.component'; import { BookService } from './services/book.service'; import { httpInterceptorProviders } from './http-interceptors/index'; import { Cache } from './services/cache'; import { CacheMapService } from './services/cache-map.service'; //For InMemory testing import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { TestData } from './test-data'; @NgModule({ imports: [ BrowserModule, FormsModule, HttpClientModule, InMemoryWebApiModule.forRoot(TestData) ], declarations: [ AppComponent, BookComponent ], providers: [ BookService, httpInterceptorProviders, CacheMapService, { provide: Cache, useClass: CacheMapService } ], bootstrap: [ AppComponent ] }) export class AppModule { }
Run Application
To run the application, find the steps.1. Install Angular In-Memory Web API.
npm i angular-in-memory-web-api --save
3. Use downloaded src in your Angular CLI application. To install Angular CLI, find the link.
4. Run ng serve using command prompt.
5. Access the URL http://localhost:4200
Search the book and look into the console log. Click the search button and wait for the output and after this, again click the search button without changing the search keyword.

References
HttpInterceptorHttpClient