Angular course ยท Module 9: HttpClient

HTTP Patterns - Three Generations

6 min read ยท
In this lesson3

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.

@Injectable({ providedIn: 'root' })
export class ApiService<T extends { id: number }> {
  constructor(private http: HttpClient, private baseUrl: string) {}

  getAll(): Observable<T[]> {
    return this.http.get<T[]>(this.baseUrl);
  }
  getById(id: number): Observable<T> {
    return this.http.get<T>(`${this.baseUrl}/${id}`);
  }
  create(item: Omit<T, 'id'>): Observable<T> {
    return this.http.post<T>(this.baseUrl, item);
  }
  update(item: T): Observable<T> {
    return this.http.put<T>(`${this.baseUrl}/${item.id}`, item);
  }
  delete(id: number): Observable<void> {
    return this.http.delete<void>(`${this.baseUrl}/${id}`);
  }
}

// A concrete service inherits the whole set of operations
@Injectable({ providedIn: 'root' })
export class SamuraiApiService extends ApiService<Samurai> {
  constructor(http: HttpClient) {
    super(http, '/api/samurai');
  }
}

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.

import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class SearchService {
  private http = inject(HttpClient);

  search(query: string, page: number) {
    const params = new HttpParams()
      .set('q', query)
      .set('page', page.toString());

    const headers = new HttpHeaders()
      .set('X-Custom-Header', 'samurai-app');

    return this.http.get('/api/search', {
      params,
      headers
    });
  }
}

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.

export class SamuraiListComponent {
  private samuraiService = inject(SamuraiApiService);

  samuraiList = signal<Samurai[]>([]);
  loading = signal(true);
  error = signal<string | null>(null);

  loadSamurai(): void {
    this.loading.set(true);
    this.error.set(null);

    this.samuraiService.getAll().pipe(takeUntilDestroyed()).subscribe({
      next: (samurai) => { this.samuraiList.set(samurai); this.loading.set(false); },
      error: (err) => { this.error.set(err.message); this.loading.set(false); }
    });
  }
}

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.

@if (loading()) {
  <app-spinner />
} @else if (error()) {
  <div class="error">{{ error() }}</div>
} @else {
  @for (samurai of samuraiList(); track samurai.id) {
    <app-samurai-card [samurai]="samurai" />
  }
}

@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.

import { resource } from '@angular/core';

export class SamuraiDetailComponent {
  id = input.required<number>();

  samuraiResource = resource({
    request: () => this.id(),
    loader: async ({ request: id }) => {
      const response = await fetch(`/api/samurai/${id}`);
      if (!response.ok) throw new Error('Failed to load');
      return response.json() as Promise<Samurai>;
    }
  });

  refresh(): void {
    this.samuraiResource.reload();
  }
}

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.

@switch (samuraiResource.status()) {
  @case ('loading') {
    <app-spinner />
  }
  @case ('error') {
    <div class="error">{{ samuraiResource.error() }}</div>
  }
  @case ('success') {
    <app-samurai-detail [samurai]="samuraiResource.value()!" />
  }
}

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.

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is HttpParams used for?

  2. 2. What does the resource() function introduced in Angular 19 offer?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Complete the search method so that it sends the query parameters (q, page) and a custom header.

  • Horizontal ordering

    Arrange the single-line tokens that create the q query parameter:

  • Vertical ordering

    Order the consecutive steps of loading data into signals in a component:

  • Vertical ordering

    Order the template blocks that handle resource() states in this order: switch header, loading, error, success:

  • Click in order

    Build the call that reloads the resource() data:

  • Horizontal ordering

    Arrange the single-line tokens that call GET with a params options object:

  • Click in order

    Build the start of a resource declaration in a component (up to opening the configuration object):

Useful articles