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.
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():
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:
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):
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():
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:
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.
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}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è diventatoparams, e nelrxResourcequi sottoloaderè diventatostream). 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:
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:
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)HttpClientcon subscribe: i comandi (salvataggi, cancellazioni, submit di form), dove la richiesta parte da un'azione dell'utente e non da un cambio di statorxResource: 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.