Every application keeps doing the same thing over and over: fetch data from the server and show one of three states - loading, error, or result. Over the years Angular has offered shorter and shorter ways to do it. In this lesson you will see three generations of the same job - from hand-written plumbing to the Resource API, which does everything for you. The same idea, less and less code.
Before we get to the states, let us clear away the repetition. Every entity in the dojo - a samurai, a clan, a sword - needs the same five operations: get all, get one, create, update, delete. Instead of writing them five times over, you build one generic service.
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// A concrete service inherits the whole set of operations
23@Injectable({ providedIn: 'root' })
24export class SamuraiApiService extends ApiService<Samurai> {
25 constructor(http: HttpClient) {
26 super(http, '/api/samurai');
27 }
28}The secret is
<T extends { id: number }> - the service is generic, it works for any type as long as that type carries an id. SamuraiApiService only extends it and hands over its own address, and gets five ready-made methods for free. Omit<T, 'id'> on create is the clever part: a brand new samurai has no id yet (the server assigns it), so the type forbids passing one.Those five methods cover the plain calls, but a search endpoint needs more than a bare URL - it needs a phrase, a page number, sometimes a custom header for the gate guards.
builds the query string, the HttpParams
?key=value tail of the address, while HttpHeaders sets the headers. You hand both to Angular inside a single options object.1import { Injectable, inject } from '@angular/core';
2import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
3
4@Injectable({ providedIn: 'root' })
5export class SearchService {
6 private http = inject(HttpClient);
7
8 search(query: string, page: number) {
9 const params = new HttpParams()
10 .set('q', query)
11 .set('page', page.toString());
12
13 const headers = new HttpHeaders()
14 .set('X-Custom-Header', 'samurai-app');
15
16 return this.http.get('/api/search', {
17 params,
18 headers
19 });
20 }
21}Read
new HttpParams().set('q', query) as one move: HttpParams is immutable, so every set returns a fresh instance and you chain them. Two of them turn into /api/search?q=katana&page=2, with the values encoded for you. Then comes this.http.get('/api/search', { params }), or { params, headers } when you need both - one options object, always the last argument of the call (the third one for post() and put(), which carry a body first). Keep the roles apart: HttpParams never sets headers, that is the job of HttpHeaders; it is not a request body, because a GET has none; and it has nothing to do with timeouts.Now the heart of it - showing the state. By hand you keep three signals: the data, "loading in progress" and "something went wrong". You flip them yourself, depending on what comes back from the server.
1export class SamuraiListComponent {
2 private samuraiService = inject(SamuraiApiService);
3
4 samuraiList = signal<Samurai[]>([]);
5 loading = signal(true);
6 error = signal<string | null>(null);
7
8 loadSamurai(): void {
9 this.loading.set(true);
10 this.error.set(null);
11
12 this.samuraiService.getAll().pipe(takeUntilDestroyed()).subscribe({
13 next: (samurai) => { this.samuraiList.set(samurai); this.loading.set(false); },
14 error: (err) => { this.error.set(err.message); this.loading.set(false); }
15 });
16 }
17}Follow the order, because it never changes:
this.loading.set(true) comes first, before the request leaves; then you subscribe; then the next handler stores the data and turns loading off; then the error handler stores the message and turns loading off as well; and finally you close the subscribe object. Forget one of those set calls and the spinner keeps spinning forever.The component holds the truth, the template shows it. Modern control flow reads those signals directly, one branch per state.
1@if (loading()) {
2 <app-spinner />
3} @else if (error()) {
4 <div class="error">{{ error() }}</div>
5} @else {
6 @for (samurai of samuraiList(); track samurai.id) {
7 <app-samurai-card [samurai]="samurai" />
8 }
9}@if (loading()) { <app-spinner /> } is the syntax you reach for today - built into the template compiler since Angular 17, with no import and no directive. The old *ngIf="loading()" still runs, but it is the legacy structural directive that this block syntax replaces. [hidden]="loading" is no answer either: it only hides an element that has already been rendered, and here it would bind the signal object instead of its value. And ng-loading="true" is not part of Angular at all. Still, count the manual work: three signals, and the duty to remember every set. This is begging for something simpler.Angular 19 noticed that this three-state pattern never changes - and packed it into
resource(). You declare only where the data comes from, and the states arrive ready-made: resource() gives you reactive data loading with signal-based status, value and error. It is not a routing module, not a dependency injection mechanism, and not another name for the classic HttpClientModule service.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
15 refresh(): void {
16 this.samuraiResource.reload();
17 }
18}Compare it with the previous listing: the three hand-held signals are gone. The declaration opens with
resource( and a configuration object whose first key is request: () => this.id() - the part that says what the load depends on. Because request reads the id signal, a change of id makes the resource fetch again by itself. The loader only has to return the data or throw. And when nothing in request has changed but you still want fresh data - after saving a form, or behind a refresh button - you call this.samuraiResource.reload().The template no longer asks three separate questions. It switches on one status and paints the matching branch.
1@switch (samuraiResource.status()) {
2 @case ('loading') {
3 <app-spinner />
4 }
5 @case ('error') {
6 <div class="error">{{ samuraiResource.error() }}</div>
7 }
8 @case ('success') {
9 <app-samurai-detail [samurai]="samuraiResource.value()!" />
10 }
11}The switch header opens on
samuraiResource.status(), then the cases follow the life of a request - loading first, then error, then success - and the closing brace shuts the block. value() may be undefined, because it holds nothing until the load succeeds, which is why the success branch adds !. The very same job as generation two, without a single manual set.Take from this lesson not the syntax but the direction: every fetch is three states - loading, error, result. You used to switch between them by hand with three signals; today
resource() does it for you. Once you recognize the pattern, the only thing left to decide is how much you want to write yourself.