Utilisation de HttpClient

1. Injection du service HttpClient

HttpClient est un service Angular ; on peut donc le récupérer avec la Dependency Injection.

book-search.component.ts
book-search.component.ts
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
​
@Component({
selector: 'wt-book-search',
templateUrl: './book-search.component.html'
})
export class BookSearchComponent {
​
constructor(private _httpClient: HttpClient) {
}
​
}

On obtient l'erreur suivante No provider for HttpClient! car le service HttpClient n'est pas encore Tree-Shakable et il faut donc importer le module associé HttpClientModule.

Etant donné que le service HttpClient est stateless, nous pouvons importer le module HttpClientModule directement dans notre Feature Module BookModule.

book.module.ts
book.module.ts
import { HttpClientModule } from '@angular/common/http';
​
@NgModule({
declarations: [
BookPreviewComponent,
BookSearchComponent
],
exports: [
BookPreviewComponent,
BookSearchComponent
],
imports: [
HttpClientModule,
SharedModule
]
})
export class BookModule {
}

2. ExĂ©cution de la requĂȘte

HttpClient.get & co.

Nous pouvons donc récupérer les données par API dans le "lifecycle hook" ngOnInit.

book-search.component.ts
book-search.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
​
@Component({
selector: 'wt-book-search',
templateUrl: './book-search.component.html'
})
export class BookSearchComponent implements OnInit {
​
private _bookListUrl = 'https://www.googleapis.com/books/v1/volumes?q=extreme%20programming';
​
constructor(private _httpClient: HttpClient) {
}
​
ngOnInit() {
this._httpClient.get(this._bookListUrl);
}
​
}

DĂ©clenchement de la requĂȘte au subscribe

En inspectant le comportement du "browser“, on peut remarquer que la requĂȘte n'est pas envoyĂ©e.

En effet, les méthodes get, delete, patch, post, put, request etc... retournent toujours un Observable.

Cet Observable est "lazy" et il faut donc subscribe pour déclencher le traitement.

Par défaut, les paramÚtres observe et responseType valent respectivement body et json ; ce qui veut dire que nous allons directement récupérer un objet JavaScript correspondant au contenu JSON "parsé" depuis la "response".

Ces deux paramĂštres (observe et responseType) peuvent ĂȘtre manipulĂ©s pour rĂ©cupĂ©rer l'objet HttpResponse, les Ă©vĂ©nements de progression ou le contenu brut d'une "response".

book-search.component.ts
book-search.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
​
@Component({
selector: 'wt-book-search',
templateUrl: './book-search.component.html'
})
export class BookSearchComponent implements OnInit {
​
bookCount: number;
bookList: Book[];
​
private _bookListUrl = 'https://www.googleapis.com/books/v1/volumes?q=extreme%20programming';
​
constructor(private _httpClient: HttpClient) {
}
​
ngOnInit() {
this._httpClient.get(this._bookListUrl)
.subscribe(googleVolumeListResponse => {
​
this.bookCount = googleVolumeListResponse.totalItems;
​
// @TODO: this.bookList = ...
​
});
}
​
}

3. Traitement de la "response"

"Hint" du type de la "response"

Lors de la compilation, TypeScript ne connait pas le type des données retournées par l'API. Par défaut, la méthode get retourne un objet de type Observable<Object> . C'est à dire que googleVolumeListResponse est de type Object (ouJavaScript Plain Object). Cela n'est pas bloquant mais on risque de perdre l'aide du compilateur et commettre des erreurs.

Les méthodes de la classe HttpClient sont des méthodes génériques et il est donc possible de contrÎler leur type de retour.

Ainsi, avec l'exemple ci-dessous :

google-volume-list-response.ts
google-volume-list-response.ts
export interface GoogleVolumeListResponse {
totalItems: number;
items: Array<{
volumeInfo: {
title: string;
}
}>;
}
book-search.component.ts
book-search.component.ts
this._httpClient.get<GoogleVolumeListResponse>(url)
.subscribe(googleVolumeListResponse => {
this.bookCount = googleVolumeListResponse.totalItem;
});

nous obtenons l'erreur suivante :

error TS2551: Property 'totalItem' does not exist on type 'GoogleVolumeListResponse'. Did you mean 'totalItems'?

ATTENTION ! Le paramÚtre de type GoogleVolumeListResponse passé à la méthode n'est qu'un "hint".

Si l'API retourne des donnĂ©es sous une forme diffĂ©rente ou si l'interface GoogleVolumeListResponse est erronĂ©e ou simplement pas Ă  jour, alors la compilation va rĂ©ussir mais en revanche nous risquons de nous retrouver avec des types incohĂ©rents dans nos variables et propriĂ©tĂ©s (et potentiellement n'importe oĂč dans l'application tant que les donnĂ©es retournĂ©es par l'API ne sont pas vĂ©rifiĂ©es en "runtime").

A titre d'exemple, si l'API renvoie le JSON suivant : {"totalItems": "oups!"} alors notre propriété bookCount qui est bien de type number, contiendra le string oups!.

Si vous disposez d'une spécification OpenAPI (Swagger) de votre API, __pensez à générer ces types avec Swagger Codegen https://swagger.io/swagger-codegen/ ou directement depuis Swagger Online Editor https://editor.swagger.io/.

Transformation de la "response"

Pour éviter les problÚmes décrits précédemment, il est préférable d'éviter de se baser uniquement sur le "hint" de type de la réponse et de le propager dans l'application.

Nous allons donc transformer la "response" en une entité associée à notre "feature".

book-search.component.ts
book-search.component.ts
export interface GoogleVolumeListResponse {
totalItems: number;
items: Array<{
volumeInfo: {
title: string;
}
}>;
}
​
@Component({
selector: 'wt-book-search',
templateUrl: './book-search.component.html',
styleUrls: ['./book-search.component.scss']
})
export class BookSearchComponent implements OnInit {
​
bookCount: number;
bookList: Book[];
​
private _bookListUrl = 'https://www.googleapis.com/books/v1/volumes?q=extreme%20programming';
​
constructor(private _httpClient: HttpClient) {
}
​
ngOnInit() {
this._httpClient.get<GoogleVolumeListResponse>(this._bookListUrl)
.subscribe(googleVolumeListResponse => {
​
this.bookCount = googleVolumeListResponse.totalItems;​
this.bookList = googleVolumeListResponse.items.map(item => new Book({
title: item.volumeInfo.title
}));
​
});
}
​
}

Attention au type de retour

Malheureusement, la classe HttpClient abuse massivement de l'anti-pattern de "method overloading".

L'anti-pattern est tellement poussĂ© Ă  l'extrĂȘme que le type de retour peut changer en fonction de la valeur de certains paramĂštres (i.e. paramĂštresobserveet responseType).

Astuce

Si vous consommez votre propre API, pensez à profiter du Duck Typing pour construire directement vos entités :

this._httpClient.get<BookListResponse>('https://books.wishtack.com/api/v2/books')
.subscribe(response => {
this.bookList = response.items.map(item => new Book(item));
});