We use cookies to enhance your experience on the site
CodeWorlds

HTTP Patterns - Three Generations

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.

Generation One: One CRUD Service for Everything

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.

HttpParams
builds the query string, the
?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.

Generation Two: Three States on Signals

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.

Generation Three: The Resource API Does It For You

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.

Go to CodeWorlds