Cours Angular · Module 9: HttpClient

Modèles HTTP - trois générations

6 min de lecture
Dans cette leçon3

Toute application refait sans cesse la même chose : aller chercher des données sur le serveur, puis afficher l'un des trois états possibles - chargement, erreur ou résultat. Au fil des versions, Angular a proposé des façons de plus en plus courtes d'écrire ce scénario. Dans cette leçon, tu vas voir trois générations d'une seule et même tâche - du travail entièrement manuel jusqu'à l'API resource, qui s'occupe de tout à ta place. La même idée, de moins en moins de code.

Première génération : un seul service CRUD pour tout

Avant de nous occuper des états, faisons le ménage dans les répétitions. Chaque entité du royaume (un samouraï, un clan, un sabre) réclame les mêmes cinq opérations : tout récupérer, en récupérer un, créer, modifier, supprimer. Plutôt que de les écrire cinq fois, tu écris un seul service générique.

1@Injectable({ providedIn: 'root' })
2export class ApiService<T extends { id: number }> {
3  constructor(private http: HttpClient, private baseUrl: string) {}
4
5  getAll(): Observable<T[]> {
6    return this.http.get<T[]>(this.baseUrl);
7  }
8  getById(id: number): Observable<T> {
9    return this.http.get<T>(`${this.baseUrl}/${id}`);
10  }
11  create(item: Omit<T, 'id'>): Observable<T> {
12    return this.http.post<T>(this.baseUrl, item);
13  }
14  update(item: T): Observable<T> {
15    return this.http.put<T>(`${this.baseUrl}/${item.id}`, item);
16  }
17  delete(id: number): Observable<void> {
18    return this.http.delete<void>(`${this.baseUrl}/${id}`);
19  }
20}
21
22// Le service concret obtient les cinq operations sans une ligne de plus
23@Injectable({ providedIn: 'root' })
24export class SamuraiApiService extends ApiService<Samurai> {
25  constructor(http: HttpClient) {
26    super(http, '/api/samurai');
27  }
28}

Le secret tient dans <T extends { id: number }> - le service est générique, il fonctionne avec n'importe quel type, à condition que celui-ci possède un id. SamuraiApiService se contente d'hériter et d'indiquer son adresse : il reçoit cinq méthodes toutes prêtes, gratuitement. Le Omit<T, 'id'> de la création est malin : un nouveau samouraï n'a pas encore d'id (c'est le serveur qui l'attribue), et le type l'interdit donc explicitement.

Il manque encore une chose à ce premier étage : filtrer la liste. Une requête GET n'a pas de body - les critères voyagent dans la query string, cette partie de l'URL qui commence par ?. Angular ne te demande pas de l'assembler à la main : HttpParams la construit pour toi, tandis que HttpHeaders s'occupe des headers. Les deux s'importent depuis @angular/common/http et partent ensemble dans l'objet d'options que tu passes à get().

1search(query: string, page: number) {
2  const params = new HttpParams()
3    .set('q', query)
4    .set('page', page.toString());
5
6  const headers = new HttpHeaders()
7    .set('X-Custom-Header', 'samurai-app');
8
9  // GET /api/search?q=...&page=...
10  return this.http.get('/api/search', { params, headers });
11}

Retiens bien ce partage des rôles, car c'est là qu'on se trompe : HttpParams sert à construire une query string (?key=value), pas à définir des headers - ça, c'est le travail de HttpHeaders - et encore moins à envoyer un body dans une requête GET ou à configurer un timeout. Remarque aussi que new HttpParams().set('q', query) renvoie un nouvel objet : HttpParams est immuable, d'où les .set() que l'on enchaîne les uns derrière les autres.

Deuxième génération : trois états sur des signals

Passons au cœur du sujet : montrer l'état. À la main, tu gardes trois signals : les données, "le chargement est en cours" et "une erreur est survenue". C'est toi qui les bascules selon ce que le serveur renvoie.

1export class SamuraiListComponent {
2  samuraiList = signal<Samurai[]>([]);
3  loading = signal(true);
4  error = signal<string | null>(null);
5
6  loadSamurai(): void {
7    this.loading.set(true);
8    this.error.set(null);
9
10    this.samuraiService.getAll().pipe(takeUntilDestroyed()).subscribe({
11      next: (samurai) => { this.samuraiList.set(samurai); this.loading.set(false); },
12      error: (err) => { this.error.set(err.message); this.loading.set(false); }
13    });
14  }
15}

Suis les trois états dans l'ordre : avant la requête tu mets loading à true, quand les données arrivent (next) tu les stockes et tu éteins le chargement, et si le serveur renvoie une erreur (error) tu enregistres le message et tu éteins le chargement là aussi. Côté template, c'est le control flow moderne qui choisit la vue : @if (loading()) { <app-spinner /> }, puis @else if (error()) et enfin @else pour la liste. Ce @if remplace l'ancien *ngIf, et il appelle bien le signal avec ses parenthèses - un [hidden]="loading" lierait la fonction elle-même, pas sa valeur. Ça fonctionne, mais compte le travail manuel : trois signals, et l'obligation de penser à éteindre loading partout. Ça appelle plus simple.

Troisième génération : l'API resource fait le travail à ta place

Angular 19 a remarqué que ce schéma à trois états est toujours le même - et l'a enfermé dans resource(). Tu indiques seulement où aller chercher les données, et les états te sont livrés tout faits.

1import { resource } from '@angular/core';
2
3export class SamuraiDetailComponent {
4  id = input.required<number>();
5
6  samuraiResource = resource({
7    request: () => this.id(),
8    loader: async ({ request: id }) => {
9      const response = await fetch(`/api/samurai/${id}`);
10      if (!response.ok) throw new Error('Failed to load');
11      return response.json() as Promise<Samurai>;
12    }
13  });
14}

Compare avec l'exemple précédent : les trois signals manuels ont disparu. resource() offre un chargement de données réactif avec des états status/value/error basés sur les signals - ce n'est ni un module de routage global, ni un mécanisme d'injection de dépendances, ni le service classique HttpClientModule sous un autre nom. Il expose lui-même status() (loading / error / success), value() et error(). Mieux : request suit le signal id, donc dès que id change, resource relance le chargement tout seul - et this.samuraiResource.reload() te laisse le redemander à la main quand tu le décides.

Dans le template, ces trois statuts appellent naturellement un @switch : l'en-tête lit le status, puis une branche par état, dans l'ordre chargement, erreur, succès.

1@switch (samuraiResource.status()) {
2  @case ('loading') { <app-spinner /> }
3  @case ('error') { {{ samuraiResource.error() }} }
4  @case ('success') { {{ samuraiResource.value() }} }
5}

Le template lit l'état, il ne le pilote pas : nulle part tu ne mets loading à true ni ne l'éteins. C'est exactement la même tâche que dans la deuxième génération, mais sans un seul set écrit de ta main.

De cette leçon, retiens moins la syntaxe que la direction : toute récupération de données, ce sont trois états - chargement, erreur, résultat. Hier tu les basculais toi-même avec trois signals, aujourd'hui resource() le fait pour toi. Une fois que tu reconnais ce schéma, il ne te reste plus qu'à choisir la part que tu veux encore écrire de ta main.

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. À quoi sert HttpParams ?

  2. 2. Que propose la fonction resource() introduite dans Angular 19 ?

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

Tâches pratiques dans le jeu

  • Éditeur de code

    Complète la méthode de recherche pour qu'elle envoie les paramètres de requête (q, page) et un en-tête personnalisé.

  • Tri horizontal

    Ordonne, sur une seule ligne, les éléments qui créent le paramètre de requête q :

  • Tri vertical

    Ordonne les étapes consécutives du chargement des données dans des signals au sein d'un composant :

  • Tri vertical

    Ordonne les blocs de template qui gèrent les états de resource() dans cet ordre : en-tête du switch, chargement, erreur, succès :

  • Clique dans l'ordre

    Construis l'appel qui recharge les données de resource() :

  • Tri horizontal

    Ordonne, sur une seule ligne, les éléments qui appellent GET avec un objet options contenant des params :

  • Clique dans l'ordre

    Construis le début d'une déclaration de resource dans un composant (jusqu'à l'ouverture de l'objet de configuration) :

Articles utiles