Angular and HTTP Requests (v4.3+)

In Angular2 the HTTP requests were done using the HTTP module. This module was missing some of the features and so in Angular4 there was a new set of APIs for HTTP requests. This was in the HTTP Request Client module. As of Angular5, the HTTP module (from Angular2) has been deprecated and so developers must use the HTTP Request Client. These notes are regarding the newer HTTP Request Client module.

Angular is dependent on RxJS as it is now part of the HttpClient method requests (the response object is Observerable). As such, we have access to the RxJS operators which can be chained to the Observables.

Setup the app.module to use the HttpClient module:

import { HttpClientModule } from '@angular/common/http';

...
imports: [
  BrowserModule,
  AppRoutingModule,
  FormsModule,
  CoreModule,
  HttpClientModule
],

Then to use the HttpClient we reference it each of the classes. This can be shown below in a sample data.service service class.

import { HttpClient } from '@angular/common/http';
import { Observable } from ‘rxjs/Observable’;
...
constructor(privatehttp:HttpClient) { }

 

Consuming REST Services

Some review of doing REST services:

  • C – create – use POST, returns HTTP 201 (Created)
  • R – read – use GET, returns HTTP 200 (Ok)
  • U – update – use PUT, returns HTTP 204 (No Content)
  • D – delete – use DELETE, returns HTTP 204 (No Content)

From the angular site we use RxJS and subscribe to Observables. The service call would call by the HTTP request type, example below is using HTTP GET:

// The Service Caller
getAll(): Observable<Book[]> {
  return this.http.get<Book[]>('/api/books');
}
// The Component Subscriber
this.dataservice.getAll().subscribe(
  (data: Bookp[]) ==> this.allBooks = data, // Success handler
  (error: any) ==> console.log(error),      // Error handler
  () ==> console.log(...)                   // completion/finally handler
);

 

RxJS has several operators that can be chained to Observables. This is powerful as it can perform complex transformations before applying it to the view. For example, we can use the ‘map’ operator to build up the return object in the service before returning the final Observable. There is also a ‘tap’ operator that can run any function of code. The example below shows how we use ‘tap’ to console.log the final object.

  getOldBookById(id: number): Observable {
    return this.http.get(`/api/books/${id}`)
      .pipe(
        map(b => { // creating the used object from server object
          bookTitle: b.title,
          year: b.publicationYear 
        }),
        tap(classicBook => console.log(classicBook))
      );
  }

 

Advanced HTTP Requests and Error Handling

Error handling from HTTP requests should be done in the service and not the component. The service will handle the error and return a more useful error message back to the component.

  getAllBooks(): Observable<Book[] | BookTrackerError> {
    console.log('Getting all books from the server.');
    return this.http.get<Book[]>('/api/books')
      .pipe(
        catchError(err => this.handleHttpError(err))
      );
  }

  private handleHttpError(error: HttpErrorResponse): Observable {
    let dataError = new BookTrackerError();
    dataError.errorNumber = 100;
    dataError.message = error.statusText;
    dataError.friendlyMessage = 'An error occurred retrieving data.';
    return ErrorObservable.create(dataError);
  }

 

Resolvers

During route transitions, resolvers can be used to fetch data before navigation, it helps prevent empty presentation to a view. The data fetched will become available in the router. To implement this, we first create the resolver. It uses the Resolve class from angular router. Since the resolver does the HTTP request, it will also handle the HTTP errors.

import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
...
@Injectable()
export class BooksResolverService implements Resolve<Book[] | BookTrackerError> {
...
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Book[] | BookTrackerError> {
    return this.dataService.getAllBooks()
      .pipe(
        catchError(err => of(err))
      );
  }

In the module’s route configuration we need to declare the use of the resolver. This can look like below.

const routes: Routes = [
  { path: 'dashboard', component: DashboardComponent, resolve: { resolvedBooks: BooksResolverService } },
  { path: 'addbook', component: AddBookComponent },
  { path: 'addreader', component: AddReaderComponent },
  { path: 'editreader/:id', component: EditReaderComponent },
  { path: 'editbook/:id', component: EditBookComponent },
  { path: '', redirectTo: 'dashboard', pathMatch: 'full' }
];

Lastly, the component that is being routed to can extract the resolver data from the ActivatedRoute object. Example shown below

  constructor(private dataService: DataService,
              private title: Title,
              private route: ActivatedRoute) { }
  
  ngOnInit() {

    let resolvedData: Book[] | BookTrackerError = this.route.snapshot.data['resolvedBooks'];

    if (resolvedData instanceof BookTrackerError) {
      console.log(`Dashboard component error: ${resolvedData.friendlyMessage}`);
    }
    else {
      this.allBooks = resolvedData;
    }    

Note how the name defined in the module’s route configuration is the name that is used to query the data from the ActivatedRoute object.

 

Creating Interceptors

HTTP requests and responses can be manipulated using interceptors. For example it can modify request headers and perform logging. Interceptors are simply services. Both Request and Response Interceptors could be defined in a single service, however it is best practice to separate them.

The example below is a Request Interceptor that adds a content-type header.

import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
...
@Injectable()
export class AddHeaderInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest, next: HttpHandler): Observable<HttpEvent> {
    let jsonReq: HttpRequest = req.clone({
      setHeaders: {'Content-Type': 'application/json'}
    });    
    return next.handle(jsonReq);
  }  
}

The example below is a Response Interceptor that checks if it is a type of HttpEventType.Response and logs the body of it.

import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpEventType } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { tap } from 'rxjs/operators';

@Injectable()
export class LogResponseInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest, next: HttpHandler): Observable<HttpEvent> {
    return next.handle(req)
      .pipe(
        tap(event => {
          if(event.type === HttpEventType.Response) {
            console.log(event.body);
          }
        })
      );
  }
}

Since interceptors are services it needs to be defined in the module. As it is provided, it needs to use the HTTP_INTERCEPTORS token in order for Angular to know it will be used as an HTTP interceptor. Note that the order defined in this module is important. The request interceptor needs to be declared first since that is executing first.

import { HTTP_INTERCEPTORS } from '@angular/common/http';
...
import { AddHeaderInterceptor } from './add-header.interceptor';
import { LogResponseInterceptor } from "app/core/log-response.interceptor";

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [],
  providers: [
    LoggerService, 
    DataService, 
    { provide: ErrorHandler, useClass: BookTrackerErrorHandlerService },
    BooksResolverService,
    { provide: HTTP_INTERCEPTORS, useClass: LogResponseInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: AddHeaderInterceptor, multi: true }
  ]
})


Caching HTTP Requests with Interceptors

Interceptors are great to use for caching. The interceptor can check requests to see if it has a cache and if so, pull it, or call the server and then have a response interceptor to store the response into cache.

In the example below we have a cache service that has a method for ‘put’ing and ‘get’ing from the cache.

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

@Injectable()
export class HttpCacheService {

  private requests: any = { }; // The cached variable

  constructor() { }

  put(url: string, response: HttpResponse): void {
    this.requests[url] = response;
  }

  get(url: string): HttpResponse | undefined { // The '|' is an OR operator
    return this.requests[url];
  }

  invalidateUrl(url: string): void {
    this.requests[url] = undefined;
  }

  invalidateCache(): void {
    this.requests = { }; // clears the whole cache
  }

}

With the cache service defined, we can now create the interceptor that will use the above caching service. Below the interceptor checks responses for a cache and adds to it if not found. Likewise on the request it checks if the cache exists and uses it if found.

@Injectable()
export class CacheInterceptor implements HttpInterceptor {

  constructor(private cacheService: HttpCacheService) { } // Use the cache service created above

  intercept(req: HttpRequest, next: HttpHandler): Observable<HttpEvent> {

    // pass along non-cacheable requests and invalidate cache
    if(req.method !== 'GET') {
      this.cacheService.invalidateCache();
      return next.handle(req);
    }

    // attempt to retrieve a cached response
    const cachedResponse: HttpResponse = this.cacheService.get(req.url);

    // return cached response
    if (cachedResponse) {
      return of(cachedResponse);
    }

    // send request to server and add response to cache
    return next.handle(req)
      .pipe(
        tap(event => {
          if (event instanceof HttpResponse) {
            this.cacheService.put(req.url, event);
          }
        })
      );

  }

 

References

Angular HTTP Communication
https://app.pluralsight.com/library/courses/angular-http-communication