Components, router, signals, forms: so far, our app lives in a world of its own. But real applications talk to a server: they read your game list, save a user, delete an order. HTTP calls bridge the frontend and the rest of the world, and in this chapter, we build that bridge.
First, we owe you an explanation: inject() appeared several times in previous chapters without a proper introduction. It is time for services and dependency injection, because that is where our HTTP logic will live.
π Services and dependency injection in two minutes
A component should handle one thing: the interface. Logic unrelated to the UI (talking to a server, sharing data between pages, calculations) belongs in a service: an ordinary class decorated with @Injectable.
1import { Injectable } from '@angular/core';
2
3@Injectable({ providedIn: 'root' })
4export class GameService {
5 // shared logic, no UI
6}providedIn: 'root' tells Angular: "create one instance for the whole app and give it to anyone who asks." How do you ask? With inject():
1import { Component, inject } from '@angular/core';
2
3@Component({ /* ... */ })
4export class GameListComponent {
5 private gameService = inject(GameService);
6}This is dependency injection: the component does not construct its dependencies; it declares them, and Angular provides them. The benefit is immediately clear in tests (you can replace the service with a fake) and sharing: two components injecting GameService talk to the same instance, and therefore the same data.
Existing projects often inject through the constructor,
constructor(private http: HttpClient) {}. It works the same way:inject()is simply the newer, more convenient form, also usable outside constructors, such as in chapter 4's guards and resolvers.
π§° Enabling HttpClient
Register Angular's HTTP client once in app.config.ts:
1import { ApplicationConfig } from '@angular/core';
2import { provideHttpClient } from '@angular/common/http';
3
4export const appConfig: ApplicationConfig = {
5 providers: [provideHttpClient()],
6};Done: HttpClient can now be injected anywhere.
π οΈ A service with CRUD operations
Combine the two ideas and write the service managing our video games with the classic CRUD operations (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}Notice the generic type, this.http.get<Game[]>(...): we tell the compiler what response to expect, and everything is typed from there on.
π Observables: the essentials
Every service method returns an Observable. What is it? An object representing a value that will arrive in the future: not the server's response, but a promise to notify you when it arrives. Listen with subscribe() to receive it:
this.gameService.getGames().subscribe((games) => {
console.log('received games:', games);
});Observables are an enormous topic (from the RxJS library; anyone following me since the old zip and combineLatest article knows how deep the rabbit hole goes). For HTTP calls, the good news is that the use case is simple: the request starts when you subscribe, emits a value when the response arrives and completes automatically.
Here is the component that loads and displays the list:
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}It works, but look critically: we handle neither loading nor errors, and manually transfer state from the Observable into a property. For years, Angular projects filled up with handwritten isLoading flags and error blocks. This is exactly what the Resource API addresses.
π The Resource API
Angular 19 introduced resource(): declare what the data depends on and how to load it, and Angular provides three ready-to-use signals: value(), isLoading() and error(). No subscriptions, no manual flags.
Example: a game search that updates as you type.
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('Unable to load games');
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="Search for a game..." />
2
3@if (games.isLoading()) {
4 <p>Caricamento...</p>
5} @else if (games.error()) {
6 <p>Something went wrong. Try again.</p>
7} @else {
8 <ul>
9 @for (game of games.value(); track game.id) {
10 <li>{{ game.title }}</li>
11 } @empty {
12 <li>No games found.</li>
13 }
14 </ul>
15}The mechanism: params reads the query signal, so every query change automatically restarts the loader. And that abortSignal passed to fetch is valuable: if the user types another letter while the previous request is in flight, Angular cancels the old one. The search race condition, where the wrong response arrives first, disappears for free.
The Resource API was still marked experimental when this chapter was written: names changed between versions 19 and 20 (
requestbecameparams, and inrxResourcebelow,loaderbecamestream). If something does not compile, check the project's Angular version.
The variants: rxResource and httpResource
The loader in resource() is Promise-based, which is why the example uses fetch. If you prefer HttpClient (with advantages such as interceptors), there are two variants. rxResource accepts an 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}For the most common case, a GET depending on signals, there is the httpResource shortcut:
import { httpResource } from '@angular/common/http';
games = httpResource<Game[]>(() => `${API_URL}/search?q=${this.query()}`);One line. The URL is a function reading signals, so changing the query restarts the request, with loading, errors and cancellation handled by Angular.
When to use each
My current compass:
resource()/httpResource: data to read and display, especially when depending on signals (searches, details, filtered lists).HttpClientwith subscribe: commands (saving, deleting, submitting forms), where the request starts from a user action rather than a state change.rxResource: the first case, but with RxJS operators needed along the way.
Where we are now
Our app has stopped talking to itself: you can create and inject services, perform CRUD operations with HttpClient, understand what subscribing to an Observable entails, and delegate loading and errors to the Resource API when reading data.
The toolbox is getting serious. And in the next chapter, the services we just introduced take center stage: we will use them to share state between components that do not know each other.
