Cours Angular · Module 9: HttpClient

Le relais des coursiers - HttpClient dès le premier parchemin

17 min de lecture
Dans cette leçon10

Tu as déjà croisé deux fois sur la piste la ligne this.http.get(...). Une première fois au pont des signaux, quand toSignal transformait la réponse du serveur en fumée sur la tour de guet, une seconde fois sur les sentiers des ninjas, quand les flux apprenaient à attendre et à filtrer. À chaque fois cette ligne fonctionnait toute seule, et à chaque fois nous avons contourné deux questions: d'où sort le champ http dans la classe, et pourquoi cette même ligne, recopiée dans une application fraîchement créée, la fait tomber juste après le démarrage.

Parce qu'un coursier, ce n'est pas seulement un homme avec une besace. Avant que quiconque ne parte d'un château vers un autre, il faut qu'un relais de poste figure au registre de la forteresse: un endroit où les coursiers attendent les ordres et d'où chaque habitant de la maison a le droit d'en appeler un. Quand personne n'a bâti ce relais, l'appel ne se termine pas par un silence, mais par une panne - Angular interrompt la construction de la classe et affiche une erreur qui contient la ligne NullInjectorError: No provider for HttpClient!. J'ai réclamé HttpClient à un injecteur vide pour voir ce message de mes propres yeux, et il ressemble exactement à cela.

Cette leçon parle de bâtir le relais et d'envoyer un premier coursier chercher un parchemin. Nous ne porterons rien aujourd'hui au château voisin - aujourd'hui, nous ne faisons que rapporter.

Ce que tu vas apprendre

  • inscrire le relais des coursiers au registre du château avec la fonction provideHttpClient()
  • choisir ses traits, à commencer par withFetch(), et savoir quand il vaut mieux ne pas activer celui-là
  • faire venir HttpClient dans la guilde avec une seule ligne d'import et un seul appel à inject()
  • envoyer une requête GET en annonçant le type attendu en retour
  • comprendre pourquoi la méthode rend un flux au lieu de données, et pourquoi personne ne court tant que personne n'écoute
  • accrocher des questions à l'adresse avec la classe HttpParams plutôt qu'avec de la concaténation de texte

Le relais de poste au registre du château

L'endroit où l'on bâtit le relais, tu le connais déjà depuis le module sur les pistes. C'est le fichier app.config.ts, et dedans la constante appConfig de type ApplicationConfig - l'objet de configuration remis à l'application au démarrage. Ce type possède exactement un champ: le tableau providers, c'est-à-dire le registre de tout ce qui doit rester à disposition pendant toute la vie de l'application. La carte des routes, tu l'y ajoutais avec la fonction provideRouter. Le relais des coursiers s'y ajoute de façon jumelle: avec la fonction provideHttpClient, que tu importes du paquet @angular/common/http. Elle-même ne crée encore aucun coursier - elle renvoie un jeu d'entrées tout prêt, que le tableau providers accepte exactement comme il accepte celles du routeur.

1// app.config.ts
2import { ApplicationConfig } from '@angular/core';
3import { provideRouter } from '@angular/router';
4import { provideHttpClient } from '@angular/common/http';
5import { routes } from './app.routes';
6
7export const appConfig: ApplicationConfig = {
8  providers: [
9    provideRouter(routes),
10    provideHttpClient()
11  ]
12};

À partir de cet instant, chaque composant et chaque service de l'application peut demander HttpClient et l'obtiendra. Remarque pourtant à quel point peu de choses ont changé: nous n'avons touché à aucun composant, nous n'avons rien ajouté à aucune classe, nous n'avons même pas indiqué l'adresse d'un serveur. Le relais ignore où courront les coursiers, et il n'a pas besoin de le savoir - les adresses, tu les donneras seulement lors des appels concrets. Aucune requête n'est partie non plus, et aucune ne partira tant que personne n'en réclamera une.

Retiens le nom de la fonction avec précision, car trois autres sonnent tout aussi crédibles et aucune ne marchera. provideHttp() et importHttpClient() n'existent pas - j'ai passé les deux au compilateur et chacune finit sur l'erreur TS2305, avec le message que le module "@angular/common/http" n'exporte aucun membre de ce nom. La troisième proposition est plus sournoise, car HttpClientModule existe pour de bon et tu le rencontreras dans de vieux exemples trouvés en ligne. Il appartient toutefois au monde des modules NgModule, et sa place est le tableau imports du décorateur @NgModule - or dans app.config.ts il n'y a rien qui ressemble à un champ imports, puisque ApplicationConfig n'a que providers. Tenter d'ajouter ce champ se termine par l'erreur TS2353, qui dit noir sur blanc que imports n'existe pas dans le type ApplicationConfig. Mieux encore: dans les types d'Angular 19, la classe HttpClientModule elle-même porte la marque @deprecated, avec une annotation qui renvoie vers un appel à provideHttpClient() placé dans providers.

Les traits du relais, ou à quoi servent les parenthèses de provideHttpClient

Les parenthèses vides de provideHttpClient() ne sont pas un ornement. Cette fonction accepte un nombre quelconque de traits - de petites fonctions qui ajoutent au relais une compétence chacune. Leurs noms commencent tous par with, et tu en découvriras plusieurs dans ce module: withInterceptors installe sur la route un poste de péage, auquel une leçon entière est consacrée plus loin, et withFetch remplace la manière même dont le coursier parcourt le chemin.

Par défaut, Angular envoie ses requêtes par le mécanisme XMLHttpRequest - un vieux cheval éprouvé, qui transporte des parchemins depuis les tout premiers navigateurs. withFetch() lui fait utiliser à la place la Fetch API, plus récente, présente aujourd'hui dans tous les navigateurs et dans Node.js depuis la version 18. Le trait se passe en argument, si bien que l'appel comporte deux étages de parenthèses: tu appelles d'abord le trait lui-même, puis tu glisses son résultat dans l'appel de provideHttpClient.

1// app.config.ts
2import { ApplicationConfig } from '@angular/core';
3import { provideHttpClient, withFetch } from '@angular/common/http';
4
5export const appConfig: ApplicationConfig = {
6  providers: [
7    provideHttpClient(withFetch())
8  ]
9};

Le plus important, c'est ce qui n'a pas changé. Aucun service de l'application n'a une autre allure après cette modification, aucune méthode ne change de nom, aucune adresse ne demande de correction, et les appels rendent toujours les mêmes flux. Nous avons échangé uniquement le cheval sous le coursier - la route, la besace et les ordres sont restés intacts.

La documentation des types d'Angular 19 recommande ce trait sans détour: pour les applications rendues côté serveur, elle conseille d'activer fetch afin d'obtenir de meilleures performances et une meilleure compatibilité. Il existe pourtant un cas où l'on ne l'active pas, et je préfère te le dire maintenant plutôt qu'après un après-midi perdu. À côté de withFetch(), ces mêmes types portent une note: la Fetch API ne rapporte pas la progression de l'envoi. J'ai regardé dans le paquet compilé et cela concorde au détail près - l'événement HttpEventType.UploadProgress n'est émis que par l'ancienne voie fondée sur XMLHttpRequest, tandis que la voie fondée sur fetch ne connaît que la progression du téléchargement. Si donc ton application doit afficher une barre de progression pendant l'envoi d'un fichier - et tu bâtiras une telle barre dès la leçon suivante - alors la branche de code qui attend UploadProgress ne s'exécutera jamais tant que withFetch() est actif, alors même que tout compilera sans le moindre avertissement. Ma recommandation tient donc en deux parties: dans une application neuve, pars de provideHttpClient(withFetch()), surtout si tu la rends côté serveur, mais si une barre de progression d'envoi figure au plan, laisse l'appel sans ce trait.

Une ligne qu'il vaut la peine de savoir par cœur

Le relais est bâti, il est donc temps de faire venir le coursier dans la guilde. La classe HttpClient habite le paquet @angular/common/http - pas @angular/core, où habitent inject, signal ou Injectable. Ce n'est pas un détail à retenir de force: j'ai passé au compilateur un import depuis @angular/core et il se termine par l'erreur TS2305, qui signale l'absence d'un tel export. La ligne d'import s'écrit ainsi, et seulement ainsi.

1import { HttpClient } from '@angular/common/http';

Relis-la jeton par jeton, car l'ordre est immuable. D'abord le mot-clé import. Ensuite le nom entre accolades - les accolades sont obligatoires, parce que HttpClient est un export nommé, un parmi beaucoup d'autres dans ce paquet, et non un export par défaut. Ensuite le mot from, qui sépare ce que tu prends de l'endroit d'où tu le prends. À la fin, le chemin du paquet entre apostrophes, puis le point-virgule. Rien ne se laisse intervertir ici: from placé avant les accolades n'a plus rien à séparer, et un chemin sans from n'est qu'une chaîne de caractères qui traîne au milieu du fichier.

Remarque ce que cette ligne ne fait pas. Elle ne crée aucun coursier. Elle fait venir dans le fichier la classe elle-même - le modèle, et du même coup le jeton avec lequel nous réclamerons dans un instant une instance toute prête.

Ce qu'est un samouraï dans le code

Avant d'envoyer le premier coursier, mettons-nous d'accord sur ce que nous allons chercher. Le registre du clan conserve des fiches de guerriers, et chaque fiche est un objet à trois champs: le numéro id attribué par le scribe du château, le prénom name et le nom du clan clan. Le numéro est un nombre, les deux autres champs sont des chaînes de caractères. C'est exactement ce que décrit une interface TypeScript, cette façon que tu connais depuis la porte de l'Académie de dire au compilateur à quoi ressemble un objet.

1interface Samurai {
2  id: number;
3  name: string;
4  clan: string;
5}

L'interface ne crée aucun objet et n'arrive jamais jusqu'au navigateur - elle disparaît à la compilation et il n'en reste que le savoir du compilateur. Elle doit nous servir à une seule chose: qu'au retour du coursier ton éditeur te propose clan au lieu de se taire, et qu'une faute de frappe dans le nom d'un champ soit attrapée avant que quiconque ne lance l'application.

Le coursier dans la guilde, ou inject à la place de new

Le service dans lequel nous installerons le coursier ressemble exactement aux guildes d'artisans du cinquième module. Le décorateur @Injectable avec l'option providedIn: 'root' désigne une seule guilde pour tout le royaume. Le champ http est rempli par la fonction inject() du paquet @angular/core, à laquelle tu passes la classe en guise de jeton et qui te rend une instance toute prête, issue du relais bâti dans la configuration. Le champ apiUrl garde le début d'adresse commun, pour ne pas le répéter dans chaque méthode.

1import { Injectable, inject } from '@angular/core';
2import { HttpClient } from '@angular/common/http';
3
4@Injectable({ providedIn: 'root' })
5export class SamuraiService {
6  private http = inject(HttpClient);
7  private apiUrl = '/api/samurais';
8}

La classe est pour l'instant vide, et c'est voulu - seul l'emplacement vient de naître, celui où nous ajouterons les méthodes dans un instant. Regarde ce qui n'a pas changé par rapport aux guildes que tu bâtissais dans le quartier des artisans: le même décorateur, le même providedIn: 'root', le même inject(). Le fait que ce service parle à un serveur n'a imposé aucune cérémonie supplémentaire. Seule la classe passée à inject() a changé.

Trois autres manières d'obtenir le coursier sont tentantes, et je les ai toutes les trois passées au compilateur. new HttpClient() finit sur l'erreur TS2554, avec le message qu'un argument était attendu alors que zéro a été fourni - car HttpClient a besoin pour travailler d'un mécanisme d'envoi, que d'ordinaire Angular lui remet lui-même. Et même si tu ajoutais cet argument, il naîtrait un coursier hors relais: un coursier qui contourne les postes de péage, auxquels une leçon séparée est consacrée, ainsi que toute la configuration d'app.config.ts. HttpClient.getInstance() finit sur l'erreur TS2339, qui dit que la propriété getInstance n'existe pas sur le type typeof HttpClient - c'est un motif venu d'autres langages, qu'Angular n'emploie pas, parce que pour distribuer des instances il dispose de l'injection de dépendances. Reste la troisième idée: puisque la classe est déjà importée, peut-être suffit-il d'appeler les méthodes directement sur elle? Cela ne suffit pas - HttpClient.get('/api/samurais') donne la même erreur TS2339, cette fois sur la propriété get. La méthode get appartient à l'instance, et l'import n'apporte que la classe. L'instance, seul l'appel inject(HttpClient) la donne - et que tu enregistres son résultat dans un champ de classe, comme ci-dessus, ou dans une simple constante avec const, cela relève du confort et ne change rien au mécanisme.

Le premier parchemin, autrement dit get

La première méthode doit rapporter le registre entier. Nous l'appellerons getAll, et à l'intérieur nous appellerons sur le coursier la méthode get() en lui passant l'adresse. Entre chevrons, juste avant l'adresse, nous inscrivons le type attendu dans la réponse - ici un tableau de samouraïs, c'est-à-dire Samurai[]. Le type renvoyé par la méthode elle-même s'écrit Observable<Samurai[]>, et la classe Observable, nous l'importons de rxjs, exactement comme sur les sentiers des ninjas.

1getAll(): Observable<Samurai[]> {
2  return this.http.get<Samurai[]>(this.apiUrl);
3}

L'écriture get<Samurai[]> est une promesse faite au compilateur, pas un contrôle à la porte. Angular ne vérifie pas que le serveur a bel et bien envoyé un tableau de fiches - j'ai placé sous le coursier un faux scribe qui, au lieu d'un tableau, a renvoyé un objet { oops: ... }, et le flux a rendu cet objet sans broncher, alors que le code annonçait Samurai[]. Les chevrons t'aident, toi et ton éditeur, mais ils ne protègent pas d'un serveur qui a changé d'avis. Si la forme de la réponse était vraiment incertaine, il faudrait écrire la vérification soi-même.

Remarque aussi ce que cette méthode n'a pas fait. Après son appel, aucune requête n'est partie. Il n'est né qu'une description du voyage - qui doit courir, où et pourquoi.

Pourquoi il revient un flux et non des données

Ici se cache la question sur laquelle le plus de monde trébuche: que renvoie au juste http.get()? La réponse est: un Observable, c'est-à-dire le flux que tu connais du module sur les éclaireurs. Pas une Promise - je l'ai vérifié sur un client en fonctionnement et l'objet rendu ne possède pas de méthode then. Pas les données non plus - car la route vers le château voisin prend du temps alors que la méthode se termine à l'instant même, si bien que pour rendre les fiches il faudrait qu'elle les possède déjà. Et pas davantage un Callback - la méthode get() n'accepte aucune fonction à rappeler plus tard, dans la parenthèse ne figurent que l'adresse et des options facultatives.

La différence entre un flux et une promesse est traîtresse, alors j'ai vérifié cela aussi: un await posé devant un tel appel n'arrangera rien - au lieu d'attendre la réponse, il rendra simplement le même flux. Pour obtenir les données, il faut soit s'abonner au flux, soit le convertir sciemment en promesse avec une fonction dédiée de rxjs.

La fonction qui recevra les données, tu la fournis seulement au moment de l'abonnement, et c'est cet abonnement seul qui met le coursier en route. On s'abonne avec la méthode subscribe(), appelée sur le flux. Dans un composant cela ressemble à ceci: tu prends le service par inject(), tu déclares implements OnInit pour promettre à TypeScript que la classe possède bien la méthode voulue, et dans cette méthode de cycle de vie ngOnInit, que tu connais du module sur les composants, tu t'abonnes au flux.

1import { Component, inject, OnInit } from '@angular/core';
2import { SamuraiService } from './samurai.service';
3
4@Component({
5  selector: 'app-roster',
6  standalone: true,
7  template: '<p>Registre du clan</p>'
8})
9export class RosterComponent implements OnInit {
10  private samurai = inject(SamuraiService);
11
12  ngOnInit(): void {
13    this.samurai.getAll().subscribe(list => {
14      console.log('Fiches au registre:', list.length);
15    });
16  }
17}

C'est cette unique ligne avec subscribe qui pousse le coursier hors des portes. Je l'ai vérifié avec un scribe de substitution qui comptait les visites: avant l'abonnement le compteur affichait zéro, après le premier abonnement un, et après en avoir ajouté un second - deux. Les flux HTTP sont paresseux et froids: deux abonnements au même flux, ce sont deux requêtes distinctes, et non une réponse servie à deux auditeurs. Si donc tu étais tenté d'enregistrer le flux dans un champ et de t'y abonner à deux endroits du template, tu enverras deux coursiers chercher le même parchemin.

subscribe lui-même n'est ici que pour rendre la mécanique visible. Dans un vrai composant, tu iras plutôt chercher AsyncPipe, vu sur les sentiers des ninjas, ou toSignal, avec lequel tu as bâti le pont vers la tour de guet - les deux voies s'abonnent à ta place et les deux nettoient l'abonnement quand le composant quitte l'écran.

Une question accrochée à l'adresse

Le registre du clan est parfois long, si bien qu'on en veut rarement la totalité. On veut les fiches qui correspondent à une expression, parfois restreintes à un seul clan. De telles conditions s'ajoutent à l'adresse après le point d'interrogation, et il est tentant de les coller avec du texte ordinaire. Ne le fais pas: l'expression "ronin égaré" enfoncée telle quelle dans l'adresse y laisse une espace et des lettres accentuées, et un caractère & dans l'expression casserait l'adresse en deux paramètres. Pour assembler tout cela sans danger, il y a la classe HttpParams, qui vient du même paquet que HttpClient. Tu crées un jeu vide avec new HttpParams(), puis tu ajoutes les paires successives avec la méthode set(), en donnant le nom du paramètre et sa valeur. Le jeu terminé, tu le glisses dans l'objet d'options sous le champ params.

1search(query: string, clan?: string): Observable<Samurai[]> {
2  let params = new HttpParams().set('q', query);
3  if (clan) {
4    params = params.set('clan', clan);
5  }
6  return this.http.get<Samurai[]>(this.apiUrl, { params });
7}

Remarque le let et l'affectation params = params.set(...), car ce n'est pas un ornement. HttpParams est immuable: set() n'ajoute rien à l'objet sur lequel tu l'appelles, il renvoie un nouveau jeu. Je l'ai vérifié - après l'appel set('clan', ...), l'ancien objet portait toujours un seul paramètre, et le second ne se trouvait que dans la copie renvoyée. Si tu écrivais simplement params.set('clan', clan); sans affectation, la restriction au clan disparaîtrait tout bonnement, et le compilateur ne dirait pas un mot.

Ce que cette classe fait des valeurs, je l'ai vérifié sur une requête réelle: l'expression avec son espace et ses accents est partie sous la forme q=ronin%20%C3%A9gar%C3%A9&clan=Takeda. Cette conversion est la seule chose qui se soit produite ici - c'est toujours un simple GET vers la même adresse, sans charge utile et sans en-tête supplémentaire. Les paramètres font partie de l'adresse, pas du contenu.

Une fiche, une adresse

Le dernier cas courant, c'est un seul guerrier demandé par son numéro. La méthode n'apporte rien de neuf - c'est toujours get() et toujours le même type entre chevrons, au singulier cette fois. Seule l'adresse change, car il faut coller le numéro au chemin. Nous l'assemblons avec un gabarit de texte: une écriture entre accents graves, dans laquelle les emplacements réservés aux valeurs se notent ${...}.

1getById(id: number): Observable<Samurai> {
2  return this.http.get<Samurai>(`${this.apiUrl}/${id}`);
3}

Surveille bien ces accents graves, car c'est l'erreur la plus fréquente de toute cette section, et une de celles que le compilateur ne signalera pas. J'ai vérifié ce qui se passe avec de simples apostrophes: la requête est partie vers l'adresse /api/samurais/${id}, avec l'écriture littérale à la place du nombre, si bien que le serveur a reçu un chemin qu'il ne connaît pas. Avec un gabarit correct, l'adresse s'est formée en /api/samurais/7. Et rien d'autre que l'adresse n'a changé - c'est toujours le même coursier, la même méthode et le même flux paresseux, qui ne partira qu'à l'abonnement.

Retiens bien: le relais dans app.config.ts donne des coursiers à l'application, inject(HttpClient) en affecte un à ta guilde, et get() ne fait que lui dicter l'ordre - celui qui l'envoie vraiment sur la route, c'est celui qui se met à écouter.

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que renvoie la méthode http.get() dans Angular ?

  2. 2. Dans l'Angular 19 moderne, comment obtient-on le plus couramment une instance de HttpClient dans un service ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri horizontal

    Ordonne, sur une seule ligne, les éléments qui importent HttpClient depuis la bibliothèque Angular :

  • Éditeur de code

    Complète le service qui récupère une liste de samouraïs depuis l'API.

  • Clique dans l'ordre

    Construis l'appel qui configure HttpClient avec la prise en charge de l'API Fetch (withFetch) :

Articles utiles