Get #Amazon #Prime for this #holiday #amazonprime #christmas #2019

#Angular 5 HttpClient and #RxJS to query the Movie DB

This is an small tutorial of using Angular 5 HttpClient to query the Movie Database API.

Angular HttpClient

Taken from Angular Http documentation:

Most front-end applications communicate with backend services over the HTTP protocol. Modern browsers support two different APIs for making HTTP requests: the XMLHttpRequest interface and the fetch() API.
The HttpClient in @angular/common/http offers a simplified client HTTP API for Angular applications that rests on the XMLHttpRequest interface exposed by browsers. Additional benefits of HttpClient include testability features, typed request and response objects, request and response interception, Observable apis, and streamlined error handling.

Angular HttpClient
Angular HttpClient

The Movie Database API
The Movie Database API

Angular HttpClient

1)
To use HttpClient, we need import to HttpClientModule and it is recommended to do it in app.module.ts file like following. Remember to import HttpClientModule after BrowserModule as HttpClient depends on the browser's XMLHttpRequest interface.
import { NgModule }         from '@angular/core';
import { BrowserModule }    from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    BrowserModule,
    // import HttpClientModule after BrowserModule.
    HttpClientModule,
  ],
  declarations: [
    AppComponent,
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}


2)
To use the Movie Database API, you need to sign up for an account to register for an API key.
i) Create an account
ii) Go to your account settings
The Movie DB account settings
The Movie DB account settings
iii) Go to API section on the left menu
The Movie DB API
iv) You can then create your API key.
Check out the Movie DB introduction document for more details.

3)
Figure out the request and response schema by testing on the Movie Database API doc.
For `GET /movie/popular` response, the interface is as following. It is a paginated response. From the first response, you will know the total pages and results.

interface MovieDbResponse {
  page: number;
  total_results: number;
  total_pages: number;
  results: Movie[];
}

Moviedb Get popular movies request and response schema
Get popular movies request and response schema
You can use their tool to figure out how to build and test the query url,  examine the response from the Movie DB. There is also some sample connection and http request code for the popular languages.
The Movie DB query url, response and code generation
The Movie DB query url, response and code generation

4)
After checking the API request and response schema,  create a request service file. In this service file, you can use HttpClient from http module to query some API endpoints. In this case, it is to query the Movie DB popular movies endpoint:

https://api.themoviedb.org/3/movie/popular?api_key=<>&language=en-US&page=1

 You can find the error handling function on Angular HttpClient document.

@Injectable()
export class MovieService {

  private apiUrl: string = `https://api.themoviedb.org/3/movie/popular?api_key=${movieApiKey}&language=en-US`;

  constructor(private http: HttpClient) {}

  getPopularMovies(total: number = 5, region: string = 'CA'): Observable {
    const url = `${this.apiUrl}&region=${region}`;

    return range(1, total).pipe(
      mergeMap(page => {
        return this.http.get(`${url}&page=${page}`)
          .pipe(
            map(res => res.results),
            retry(3), // retry a failed request up to 3 times
            catchError(this.handleError) // then handle the error
          );
      }),
    );
  }

  private handleError(error: HttpErrorResponse) {
    if (error.error instanceof ErrorEvent) {
      console.error('An error occurred:', error.error.message);
    } else {
      console.error(
        `Backend returned code ${error.status}, ` +
        `body was: ${error.error}`);
    }
    return new ErrorObservable(
      'Something bad happened; please try again later.');
  };
}

i) rxjs pipe

According to rxjs pipe document, a pipeable operator is basically any function that returns a function with the signature: (source: Observable) => Observable. Check out the example taken from rxjs pipe document below:

import { range } from 'rxjs/observable/range';
import { map, filter, scan } from 'rxjs/operators';

const source$ = range(0, 10);

source$.pipe(
  filter(x => x % 2 === 0),
  map(x => x + x),
  scan((acc, x) => acc + x, 0)
)
.subscribe(x => console.log(x))

It is a replacement of dot-chaining of rxjs functions. Keep in mind that only functions are are under 'rxjs/operators' are pipe-able. Why pipe?
To summarize:
- it reduces blind dependencies by enforcing explicit imports
- enables webpack or rollup tree-shaking
- enables linting
- makes building custom operators easier.

Read more about pipe on this medium Angular article.

ii) rxjs mergeMap

Taken from the mergeMap document that mergeMap allows for multiple inner subscriptions to be active at a time.  mergeMap flattens an observable of observables into a stream of responses. Thus, it enables me to query multiple pages of popular movies and then merge them together.

rxjs mergeMap

Read more about mergeMap on the same medium Angular article about mergeMap vs concatMap vs forkjoin. According to this doc, mergeMap executes all inner observables immediately as they pass through the stream and thus running in parallel. This would reduce the request time.

5)
Pagination
When the response is a long list of items, there is usually pagination parameter in the query url. In this case, we can set the page parameter to get the corresponding items on that page.

https://api.themoviedb.org/3/movie/popular?api_key=<>&language=en-US&page=1

6)
Process the movie response to get only the titles

getPopularMovieTitles(total: number = 5, region: string = 'CA'): Observable {
  return this.getPopularMovies(total, region)
        .map(movies => {
      return movies.map(movie => {
        return { 'id': String(movie.id), 'data': movie.title };
      });
    });
}

7) rxjs
rxjs document github: https://github.com/btroncone/learn-rxjs
rxjs document: https://www.learnrxjs.io/
rxjs beta document: http://rxjsdocs.com/

8)
In the end, the popular movie query service looks like this: 

import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import 'rxjs/add/operator/map';
import { catchError, map, mergeMap, retry } from 'rxjs/operators';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { range } from 'rxjs/observable/range';
import { Movie } from '../models/movie.model';
import { Observable } from 'rxjs/Observable';
import { movieApiKey } from '../../assets/constants.private';
import { ListItem } from '../models/list-item.model';

interface MovieDbResponse {
  page: number;
  total_results: number;
  total_pages: number;
  results: Movie[];
}

@Injectable()
export class MovieService {

  private apiUrl: string = `https://api.themoviedb.org/3/movie/popular?api_key=${movieApiKey}&language=en-US`;

  constructor(private http: HttpClient) {}

  getPopularMovies(total: number = 5, region: string = 'CA'): Observable {
    const url = `${this.apiUrl}&region=${region}`;

    return range(1, total).pipe(
      mergeMap(page => {
        return this.http.get(`${url}&page=${page}`)
          .pipe(
            map(res => res.results),
            retry(3), // retry a failed request up to 3 times
            catchError(this.handleError) // then handle the error
          );
      }),
    );
  }

  getPopularMovieTitles(total: number = 5, region: string = 'CA'): Observable {
    return this.getPopularMovies(total, region)
      .map(movies => {
        return movies.map(movie => {
          return { 'id': String(movie.id), 'data': movie.title };
        });
      });
  }

  private handleError(error: HttpErrorResponse) {
    if (error.error instanceof ErrorEvent) {
      console.error('An error occurred:', error.error.message);
    } else {
      console.error(
        `Backend returned code ${error.status}, ` +
        `body was: ${error.error}`);
    }
    return new ErrorObservable(
      'Something bad happened; please try again later.');
  };
}


Thanks for reading!

Jun