Tutorial: HTTP

REFERENCES
[1]: https://angular.io/tutorial/toh-pt6#heroes-and-http "HTTP"

In this tutorial, you'll add the following data persistence features with help from Angular's HttpClient.

  • The HeroService gets hero data with HTTP requests.
  • Users can add, edit, and delete heroes and save these changes over HTTP.
  • Users can search for heroes by name.

Enable HTTP services

HttpClient is Angular's mechanism for communicating with a remote server over HTTP.

To make HttpClient available everywhere in the app,

  • open the root AppModule,
  • import the HttpClientModule symbol from @angular/common/http,
  • add it to the @NgModule.imports array.
src/app/app.module.ts
...

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

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...

    HttpClientModule,
  ],
  providers: [ HeroService, MessageService ],
  bootstrap: [ AppComponent ]
})

export class AppModule { }

Simulate a data server

This tutorial sample mimics communication with a remote data server by using the In-memory Web API module.

After installing the module, the app will make requests to and receive responses from the HttpClient without knowing that the In-memory Web API is intercepting those requests, applying them to an in-memory data store, and returning simulated responses.

This facility is a great convenience for the tutorial. You won't have to set up a server to learn about HttpClient.

It may also be convenient in the early stages of your own app development when the server's web api is ill-defined or not yet implemented.

Important: the In-memory Web API module has nothing to do with HTTP in Angular.

If you're just reading this tutorial to learn about HttpClient, you can skip over this step. If you're coding along with this tutorial, stay here and add the In-memory Web API now.

Install the In-memory Web API package from npm

npm install angular-in-memory-web-api --save-dev

Please note, I have chosen to install the angular-in-memory-web-api as a Development Dependency rather than a core dependency. To learn more about the difference, see the documentation for npm.

Import the InMemoryWebApiModule and the InMemoryDataService class, which you will create in a moment.

Add the InMemoryWebApiModule to the @NgModule.imports array— after importing the HttpClient, —while configuring it with the InMemoryDataService.

src/app/app.module.ts
...

import { HttpClientModule } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...

    AppRoutingModule,
    HttpClientModule,

    // The HttpClientInMemoryWebApiModule module intercepts HTTP requests
    // and returns simulated server responses.
    // Remove it when a real server is ready to receive requests.
    HttpClientInMemoryWebApiModule.forRoot(
      InMemoryDataService, { dataEncapsulation: false }
    ),
  ],
  providers: [ HeroService, MessageService ],
  bootstrap: [ AppComponent ]
})

export class AppModule { }

The forRoot() configuration method takes an InMemoryDataService class that primes the in-memory database.

The Tour of Heroes sample creates such a class src/app/in-memory-data.service.ts which has the following content:

src/app/in-memory-data.service.ts
import { InMemoryDbService } from 'angular-in-memory-web-api';

export class InMemoryDataService implements InMemoryDbService {
  createDb() {
    const heroes = [
      {
        id: 1,
        name: 'Captain America',
        realName: 'Steven "Steve" Rogers',
        powers: '...',
        abilities: '...',
        imgPath: '../../assets/img/heroes/captain_america_img.jpg',
        avatarPath: '../../assets/img/heroes/captain_america_avatar.jpg',
      },
      {
        id: 2,
        name: 'Iron Man',
        realName: 'Anthony Edward "Tony" Stark',
        powers: '...',
        abilities: '...',
        imgPath: '../../assets/img/heroes/iron_man_img.jpg',
        avatarPath: '../../assets/img/heroes/iron_man_avatar.jpg',
      },
      {
        id: 3,
        name: 'Thor',
        realName: 'Thor Odinson',
        powers: '...',
        abilities: '...',
        imgPath: '../../assets/img/heroes/thor_img.jpg',
        avatarPath: '../../assets/img/heroes/thor_avatar.jpg',
      },
      {
        id: 4,
        name: 'Hulk',
        realName: 'Robert Bruce Banner',
        powers: '...',
        abilities: '...',
        imgPath: '../../assets/img/heroes/hulk_img.jpg',
        avatarPath: '../../assets/img/heroes/hulk_avatar.jpg',
      },
      {
        id: 5,
        name: 'Black Widow',
        realName: 'Natasha Romanova',
        powers: '...',
        abilities: '...',
        imgPath: '../../assets/img/heroes/black_widow_img.jpg',
        avatarPath: '../../assets/img/heroes/black_widow_avatar.jpg',
      },
      {
        id: 6,
        name: 'Ant-Man',
        realName: 'Scott Lang',
        powers: '...',
        abilities: '...',
        imgPath: '../../assets/img/heroes/ant-man_img.jpg',
        avatarPath: '../../assets/img/heroes/ant-man_avatar.jpg',
      },
      {
        id: 7,
        name: 'Hawkeye',
        realName: 'Clinton Francis "Clint" Barton',
        powers: '...',
        abilities: '...',
        imgPath: '../../assets/img/heroes/hawkeye_img.jpg',
        avatarPath: '../../assets/img/heroes/hawkeye_avatar.jpg',
      },
      {
        id: 8,
        name: 'Doctor Strange',
        realName: 'Stephen Vincent Strange',
        powers: '...',
        abilities: '...',
        imgPath: '../../assets/img/heroes/doctor_strange_img.jpg',
        avatarPath: '../../assets/img/heroes/doctor_strange_avatar.jpg',
      },
      {
        id: 9,
        name: 'Black Panther',
        realName: 'T\'Challa',
        powers: '...',
        abilities: '...',
        imgPath: '../../assets/img/heroes/black_panther_img.jpg',
        avatarPath: '../../assets/img/heroes/black_panther_avatar.jpg',
      },
      {
        id: 10,
        name: 'Spider-Man',
        realName: 'Peter Benjamin Parker',
        powers: ''...'',
        abilities: '...',
        imgPath: '../../assets/img/heroes/spider-man_img.jpg',
        avatarPath: '../../assets/img/heroes/spider-man_avatar.jpg',
      }
    ];
    return {heroes};
  }
}

Please note, I used data that I gathered from http://marvel.wikia.com/wiki/Marvel_Database.

This file replaces mock-heroes.ts, which is now safe to delete. (I have left this in my project for reference).

When your server is ready, detach the In-memory Web API, and the app's requests will go through to the server.

Now back to the HttpClient story.

Heroes and HTTP

Import some HTTP symbols that you'll need.

Inject HttpClient into the constructor in a private property called http.

Keep injecting the MessageService. You'll call it so frequently that you'll wrap it in private log method.

Define the heroesUrl with the address of the heroes resource on the server.

src/app/hero.service.ts
import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { MessageService } from './message.service';
import { HttpClient, HttpHeaders } from '@angular/common/http';

@Injectable()
export class HeroService {
  private heroesUrl = 'api/heroes';  // URL to web api

  constructor(
    private http: HttpClient,
    private messageService: MessageService) { }

  /** Log a HeroService message with the Angular Material Snackbar */
  private log(message: string) {
    this.messageService.showSnackBar('HeroService: ' + message);
  }

  getHeroes(): Observable<Hero[]> {
    // this.messageService.add('HeroService: fetched heroes');
    this.log('Heroes Fetched');
    return of(HEROES);
  }

  getHero(id: number): Observable<Hero> {
    this.log(`fetched hero id=${id}`);
    return of(HEROES.find(hero => hero.id === id));
  }
}

Get heroes with HttpClient

The current HeroService.getHeroes() uses the RxJS of() function to return an array of mock heroes as an Observable<Hero[]>. Convert that method to use HttpClient.

src/app/hero.service.ts
...

@Injectable()
export class HeroService {
  private heroesUrl = 'api/heroes';  // URL to web api

  constructor(
    private http: HttpClient,
    private messageService: MessageService) { }

  /** Log a HeroService message with the Angular Material Snackbar */
  private log(message: string) {
    this.messageService.showSnackBar('HeroService: ' + message);
  }

  // New HttpClient method
  getHeroes(): Observable<Hero[]> {
    // this.messageService.add('HeroService: fetched heroes');
    this.log('Heroes Fetched');
    return this.http.get<Hero[]>(this.heroesUrl);
  }

  // Old RxJS method
  getHero(id: number): Observable<Hero> {
    this.log(`fetched hero id=${id}`);
    return of(HEROES.find(hero => hero.id === id));
  }
}

Refresh the browser. The hero data should successfully load from the mock server.

You've swapped of for http.get and the app keeps working without any other changes because both functions return an Observable<Hero[]>.

Http methods return one value

All HttpClient methods return an RxJS Observable of something.

HTTP is a request/response protocol. You make a request, it returns a single response.

In general, an Observable can return multiple values over time. An Observable from HttpClient always emits a single value and then completes, never to emit again.

This particular HttpClient.get call returns an Observable<Hero[]>, literally "an observable of hero arrays". In practice, it will only return a single hero array.

HttpClient.get returns response data

HttpClient.get returns the body of the response as an untyped JSON object by default. Applying the optional type specifier, <Hero[]> , gives you a typed result object.

The shape of the JSON data is determined by the server's data API. The Tour of Heroes data API returns the hero data as an array.

Other APIs may bury the data that you want within an object. You might have to dig that data out by processing the Observable result with the RxJS map operator.

Although not discussed here, there's an example of map in the getHeroNo404() method included in the sample source code.

Error handling

Things go wrong, especially when you're getting data from a remote server. The HeroService.getHeroes() method should catch errors and do something appropriate.

To catch errors, you "pipe" the observable result from http.get() through an RxJS catchError() operator.

Import the catchError symbol from rxjs/operators, along with some other operators you'll need later.

Now extend the observable result with the .pipe() method and give it a catchError() operator.

src/app/hero.service.ts
...

import { catchError } from 'rxjs/operators';

@Injectable()
export class HeroService {
  private heroesUrl = 'api/heroes';  // URL to web api

  ...

  // New HttpClient method
  getHeroes(): Observable<Hero[]> {
    // this.messageService.add('HeroService: fetched heroes');
    this.log('Heroes Fetched');
    return this.http.get<Hero[]>(this.heroesUrl).pipe(
      catchError(this.handleError('getHeroes', []))
    );
  }

  ...
}

The catchError() operator intercepts an Observable that failed. It passes the error an error handler that can do what it wants with the error.

The following handleError() method reports the error and then returns an innocuous result so that the application keeps working.

handleError

The following errorHandler() will be shared by many HeroService methods so it's generalized to meet their different needs.

Instead of handling the error directly, it returns an error handler function to catchError that it has configured with both the name of the operation that failed and a safe return value.

src/app/hero.service.ts
...

@Injectable()
export class HeroService {
  private heroesUrl = 'api/heroes';  // URL to web api

  ...

  // New HttpClient method
  getHeroes(): Observable<Hero[]> {
    // this.messageService.add('HeroService: fetched heroes');
    this.log('Heroes Fetched');
    return this.http.get<Hero[]>(this.heroesUrl).pipe(
      catchError(this.handleError('getHeroes', []))
    );
  }

  ...

  /**
   * Handle Http operation that failed.
   * Let the app continue.
   * @param operation - name of the operation that failed
   * @param result - optional value to return as the observable result
   */
  private handleError<T> (operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {

      // TODO: send the error to remote logging infrastructure
      console.error(error); // log to console instead

      // TODO: better job of transforming error for user consumption
      this.log(`${operation} failed: ${error.message}`);

      // Let the app keep running by returning an empty result.
      return of(result as T);
    };
  }
}

After reporting the error to console, the handler constructs a user friendly message and returns a safe value to the app so it can keep working.

Because each service method returns a different kind of Observable result, errorHandler() takes a type parameter so it can return the safe value as the type that the app expects.

Tap into the Observable

The HeroService methods will tap into the flow of observable values and send a message (via log()) to the message area at the bottom of the page.

They'll do that with the RxJS tap operator, which looks at the observable values, does something with those values, and passes them along. The tap call back doesn't touch the values themselves.

Here is the final version of getHeroes with the tap that logs the operation. Don't forget to import tap from rxjs/operators.

src/app/hero.service.ts
...

import {catchError, tap} from 'rxjs/operators';

@Injectable()
export class HeroService {

  ...

  // New HttpClient method
  getHeroes(): Observable<Hero[]> {
    // this.messageService.add('HeroService: fetched heroes');
    return this.http.get<Hero[]>(this.heroesUrl).pipe(
      tap(() => this.log('fetched heroes.')),
      catchError(this.handleError('getHeroes', []))
    );
  }

  ...

  private handleError<T> (operation = 'operation', result?: T) {
    ...
  }
}

Get hero by id

Most web APIs support a get by id request in the form api/hero/:id (such as api/hero/11). Add a HeroService.getHero() method to make that request:

src/app/hero.service.ts
...

@Injectable()
export class HeroService {
  private heroesUrl = 'api/heroes';  // URL to web api

  ...

  // New HttpClient method
  getHeroes(): Observable<Hero[]> {
    ...
  }

  // New HttpClient method
  getHero(id: number): Observable<Hero> {
    const url = `${this.heroesUrl}/${id}`;
    return this.http.get<Hero>(url).pipe(
      tap(() => this.log(`fetched hero id=${id}`)),
      catchError(this.handleError<Hero>(`getHero id=${id}`))
    );
  }

  ...

  private handleError<T> (operation = 'operation', result?: T) {
    ...
  }
}

There are three significant differences from getHeroes().

  • it constructs a request URL with the desired hero's id.
  • the server should respond with a single hero rather than an array of heroes.
  • therefore, getHero returns an Observable<Hero> ("an observable of Hero objects") rather than an observable of hero arrays .

To be continued...

At this point, the rest of the Tour of Heroes tutorial will require some form controls. We will divulge from the Angular tutorials to define a more expansive form for adding a hero. For this, we will now look at the Angular Material Form Controls next.

results matching ""

    No results matching ""