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 RxJSmap
operator.Although not discussed here, there's an example of
map
in thegetHeroNo404()
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 anObservable<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.