BACK TO THE SERIESGuida galattica ad Angular · 銀河のガイド

Angular in 10 steps - 7 of 10

angular in 10 steps 7 of 10

07 - Chiamate HTTP: dai servizi alla Resource API


Componenti, router, signals, form: finora la nostra app vive in un mondo tutto suo. Ma le applicazioni vere parlano con un server: leggono la lista dei tuoi giochi, salvano un utente, cancellano un ordine. Le chiamate HTTP sono il ponte tra il frontend e il resto del mondo, e in questo capitolo lo costruiamo.


Prima però dobbiamo saldare un debito: nei capitoli scorsi inject() è spuntato più volte senza mai una presentazione ufficiale. È il momento di parlare di servizi e dependency injection, perché è lì che vivrà la nostra logica HTTP.


💉 Servizi e dependency injection in due minuti


Un componente dovrebbe occuparsi di una cosa sola: l'interfaccia. La logica che non riguarda la UI (parlare col server, condividere dati tra pagine, fare calcoli) sta meglio in un service: una classe normale, decorata con @Injectable.

TYPESCRIPT · コード
1import { Injectable } from '@angular/core';
2
3@Injectable({ providedIn: 'root' })
4export class GameService {
5  // logica condivisa, niente UI
6}

Quel providedIn: 'root' dice ad Angular: "creane una sola istanza per tutta l'app e dalla a chiunque la chieda". E come si chiede? Con inject():

TYPESCRIPT · コード
1import { Component, inject } from '@angular/core';
2
3@Component({ /* ... */ })
4export class GameListComponent {
5  private gameService = inject(GameService);
6}

Questa è la dependency injection: il componente non costruisce le sue dipendenze, le dichiara, e Angular gliele fornisce. Il vantaggio si vede subito nei test (puoi sostituire il service con un finto) e nella condivisione: due componenti che iniettano GameService parlano con la stessa istanza, quindi con gli stessi dati.

Nei progetti esistenti troverai spesso l'iniezione via costruttore, constructor(private http: HttpClient) {}. Funziona identica: inject() è solo la forma più recente e più comoda, perché si usa anche fuori dal costruttore, come nelle guardie e nei resolver del capitolo 4.

🧰 Abilitare HttpClient


Il client HTTP di Angular va registrato una volta sola, in app.config.ts:

TYPESCRIPT · コード
1import { ApplicationConfig } from '@angular/core';
2import { provideHttpClient } from '@angular/common/http';
3
4export const appConfig: ApplicationConfig = {
5  providers: [provideHttpClient()],
6};

Fatto: da questo momento HttpClient è iniettabile ovunque.


🛠️ Un service con le operazioni CRUD


Mettiamo insieme le due cose e scriviamo il service che gestisce i nostri videogiochi, con le classiche operazioni CRUD (create, read, update, delete):

TYPESCRIPT · コード
1import { inject, Injectable } from '@angular/core';
2import { HttpClient } from '@angular/common/http';
3import { Observable } from 'rxjs';
4import { Game } from './models/game.model';
5
6@Injectable({ providedIn: 'root' })
7export class GameService {
8  private http = inject(HttpClient);
9  private apiUrl = 'https://api.example.com/games';
10
11  getGames(): Observable<Game[]> {
12    return this.http.get<Game[]>(this.apiUrl);
13  }
14
15  getGame(id: string): Observable<Game> {
16    return this.http.get<Game>(`${this.apiUrl}/${id}`);
17  }
18
19  addGame(game: Game): Observable<Game> {
20    return this.http.post<Game>(this.apiUrl, game);
21  }
22
23  updateGame(id: string, game: Game): Observable<Game> {
24    return this.http.put<Game>(`${this.apiUrl}/${id}`, game);
25  }
26
27  deleteGame(id: string): Observable<void> {
28    return this.http.delete<void>(`${this.apiUrl}/${id}`);
29  }
30}

Nota il tipo generico, this.http.get<Game[]>(...): stiamo dicendo al compilatore cosa aspettarci dalla risposta, e da lì in poi tutto è tipizzato.


🔄 Observables, il minimo indispensabile


Ogni metodo del service restituisce un Observable. Cos'è? Un oggetto che rappresenta un valore che arriverà in futuro: non la risposta del server, ma la promessa di avvisarti quando arriva. Per riceverla ti metti in ascolto con subscribe():

TYPESCRIPT · コード
this.gameService.getGames().subscribe((games) => {
  console.log('ho ricevuto i giochi:', games);
});

Gli Observable sono un mondo enorme (vengono dalla libreria RxJS, e chi mi segue dal vecchio articolo su zip e combineLatest sa quanto ci si può perdere), ma per le chiamate HTTP la buona notizia è che il caso d'uso è semplice: la richiesta parte quando ti sottoscrivi, emette un valore quando arriva la risposta, e si chiude da sola.


Ecco il componente che carica e mostra la lista:

TYPESCRIPT · コード
1import { Component, inject, OnInit } from '@angular/core';
2import { GameService } from './game.service';
3import { Game } from './models/game.model';
4
5@Component({
6  selector: 'app-game-list',
7  template: `
8    <h2>I miei giochi</h2>
9    <ul>
10      @for (game of games; track game.id) {
11        <li>{{ game.title }}</li>
12      }
13    </ul>
14  `
15})
16export class GameListComponent implements OnInit {
17  private gameService = inject(GameService);
18  games: Game[] = [];
19
20  ngOnInit() {
21    this.gameService.getGames().subscribe((data) => {
22      this.games = data;
23    });
24  }
25}

Funziona, ma guardalo con occhio critico: non gestiamo il caricamento, non gestiamo gli errori, e lo stato va travasato a mano dall'Observable a una proprietà. Per anni i progetti Angular si sono riempiti di flag isLoading e blocchi error scritti a mano. Ed è esattamente il problema che la Resource API viene a risolvere.


🚀 La Resource API


Da Angular 19 esiste resource(): dichiari da cosa dipende il dato e come caricarlo, e Angular ti dà tre signals già pronti: value(), isLoading() ed error(). Niente subscribe, niente flag manuali.


Esempio: una ricerca di giochi che si aggiorna mentre digiti.

TYPESCRIPT · コード
1import { Component, inject, resource, signal } from '@angular/core';
2import { Game } from './models/game.model';
3
4const API_URL = 'https://api.example.com/games';
5
6@Component({
7  selector: 'app-game-search',
8  templateUrl: './game-search.component.html'
9})
10export class GameSearchComponent {
11  query = signal('');
12
13  games = resource({
14    params: () => this.query(),
15    loader: async ({ params, abortSignal }) => {
16      const response = await fetch(`${API_URL}/search?q=${params}`, {
17        signal: abortSignal,
18      });
19      if (!response.ok) throw new Error('Impossibile caricare i giochi');
20      return (await response.json()) as Game[];
21    },
22  });
23
24  search(event: Event) {
25    this.query.set((event.target as HTMLInputElement).value);
26  }
27}
HTML · コード
1<input (input)="search($event)" placeholder="Cerca un gioco..." />
2
3@if (games.isLoading()) {
4  <p>Caricamento...</p>
5} @else if (games.error()) {
6  <p>Qualcosa è andato storto. Riprova.</p>
7} @else {
8  <ul>
9    @for (game of games.value(); track game.id) {
10      <li>{{ game.title }}</li>
11    } @empty {
12      <li>Nessun gioco trovato.</li>
13    }
14  </ul>
15}

Il meccanismo: params legge il signal query, quindi ogni volta che la query cambia il loader riparte da solo. E quel abortSignal passato a fetch è un regalo prezioso: se l'utente digita un'altra lettera mentre la richiesta precedente è in volo, Angular annulla quella vecchia. La race condition delle ricerche, quella per cui arriva prima la risposta sbagliata, sparisce gratis.

La Resource API è recente e ancora marcata come sperimentale: i nomi sono cambiati tra la versione 19 e la 20 (request è diventato params, e nel rxResource qui sotto loader è diventato stream). Se qualcosa non compila, controlla la versione di Angular del progetto.

Le varianti: rxResource e httpResource


Il loader di resource() è basato su Promise, per questo nell'esempio ho usato fetch. Se preferisci restare su HttpClient (che ha i suoi vantaggi, come gli interceptor) ci sono due varianti. rxResource accetta un Observable:

TYPESCRIPT · コード
1import { inject } from '@angular/core';
2import { HttpClient } from '@angular/common/http';
3import { rxResource } from '@angular/core/rxjs-interop';
4
5export class GameSearchComponent {
6  private http = inject(HttpClient);
7  query = signal('');
8
9  games = rxResource({
10    params: () => this.query(),
11    stream: ({ params }) =>
12      this.http.get<Game[]>(`${API_URL}/search?q=${params}`),
13  });
14}

E per il caso più comune di tutti, una GET che dipende da qualche signal, c'è la scorciatoia httpResource:

TYPESCRIPT · コード
import { httpResource } from '@angular/common/http';

games = httpResource<Game[]>(() => `${API_URL}/search?q=${this.query()}`);

Una riga. L'URL è una funzione che legge i signals, quindi al cambio della query la richiesta riparte, con loading, errori e annullamento gestiti da Angular.


Quando usare cosa


La mia bussola attuale:


  • resource() / httpResource: dati da leggere e mostrare, soprattutto se dipendono da signals (ricerche, dettagli, liste filtrate)
  • HttpClient con subscribe: i comandi (salvataggi, cancellazioni, submit di form), dove la richiesta parte da un'azione dell'utente e non da un cambio di stato
  • rxResource: quando sei nel primo caso ma ti servono gli operatori RxJS di mezzo

Dove siamo arrivati


Oggi la nostra app ha smesso di parlare da sola: sai creare un service e iniettarlo dove serve, fare le operazioni CRUD con HttpClient, capire cosa firma quando fai subscribe() a un Observable, e delegare loading ed errori alla Resource API quando si tratta di leggere dati.


La cassetta degli attrezzi comincia a essere seria. E i service, che qui abbiamo appena presentato, nel prossimo capitolo diventano protagonisti: vedremo come usarli per condividere lo stato tra componenti che non si conoscono tra loro.