Angular Caching Http Interceptor

By Arvind Rai, 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.

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 using Map. 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;
} 
In the above class we have 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 
url: Request URL whose response is to be cached.
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);
            }
        })
    }
} 
Find the following points.
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 implement HttpInterceptor 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);
    }
} 
HttpRequest: It is an outgoing HTTP request. It provides getters to fetch request properties such as 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 }
]; 
Now configure it with providers attribute.
@NgModule({
  providers: [
      httpInterceptorProviders
  ],
  ------
})
export class AppModule { } 

Other Components, Services and Module used in Example

index.ts
import { 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 }
]; 
logging-interceptor.ts
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);
  }
} 
book.ts
export interface Book {
   id: number;
   name: string;
   category: string;
   year: string;
} 
book.service.ts
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
        });
    }
} 
book.component.ts
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);
   }
} 
book.component.html
<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> 
test-data.ts
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 };
  }
} 
app.component.ts
import { Component } from '@angular/core';

@Component({
   selector: 'app-root',
   template: `
	<app-book></app-book>
    `
})
export class AppComponent { 
} 
app.module.ts
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 
2. Download source code using download link given below on this page.
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.
Angular Caching Http Interceptor
Look into the time taken by two consecutive search requests for same keyword. First time search is generated by running request URL and second time we get cached response. That is why time taken by first request is much higher than the second request. Cache will expire after 20 seconds.

References

HttpInterceptor
HttpClient

Download Source Code

POSTED BY
ARVIND RAI
ARVIND RAI









©2023 concretepage.com | Privacy Policy | Contact Us