HTTP Client and RxJS Basics

Chapter 11
HTTP Client and RxJS Basics
Talking to a backend the Angular way — with Observables, not Promises

React used fetch returning a Promise, awaited in a useEffect (Intermediate Chapter 6). Angular's HttpClient instead returns an Observable — a stream of values from RxJS, the reactive library Angular is built on. The same Observable already appeared as valueChanges (Chapter 9) and route.paramMap (Chapter 10); this chapter covers it properly, alongside the HTTP client that produces them most often.

Observable vs Promise — The Core Difference

A Promise resolves once with a single value. An Observable is a stream that can emit many values over time (or just one, like an HTTP response) — and crucially, it does nothing at all until something subscribes to it. Where await kicks off a Promise immediately, an Observable is lazy: no subscription, no work. That laziness is what makes operators like cancellation, retrying, and debouncing possible in ways Promises can't match.

Setting Up HttpClient

// main.ts — provide it once for the whole app import { provideHttpClient } from '@angular/common/http'; bootstrapApplication(AppComponent, { providers: [provideHttpClient()], });

provideHttpClient() registers the HTTP client with Angular's DI system once, the same pattern as provideRouter (Chapter 10). After that, any service can inject HttpClient and make requests.

An API Call Inside a Service

// user.service.ts import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; interface User { id: number; name: string; } @Injectable({ providedIn: 'root' }) export class UserService { private http = inject(HttpClient); getUsers(): Observable<User[]> { return this.http.get<User[]>('https://api.example.com/users'); } }

Per the Chapter 6 convention, API calls live in a service, not a component. http.get<User[]>(url) returns an Observable<User[]> — the <User[]> generic tells TypeScript what shape the response data will be, giving full type safety on the result. Note the service just returns the Observable without subscribing; the component decides when to do that.

Consuming It — Subscribe, or the async Pipe

// option A: subscribe manually in the component export class UserListComponent implements OnInit { private userService = inject(UserService); users: User[] = []; ngOnInit() { this.userService.getUsers().subscribe((users) => { this.users = users; }); } }
// option B: let the async pipe subscribe (and unsubscribe) for you export class UserListComponent { private userService = inject(UserService); users$ = this.userService.getUsers(); // just the Observable, not subscribed }
<!-- template for option B --> @if (users$ | async; as users) { <ul> @for (user of users; track user.id) { <li>{{ user.name }}</li> } </ul> }

The async pipe (Chapter 7) subscribes to the Observable, hands its emitted value to the template, and — critically — unsubscribes automatically when the component is destroyed. This is the preferred approach: it avoids manual subscription bookkeeping entirely, and the ; as users syntax captures the emitted array into a template variable to loop over. The naming convention users$ (trailing $) marks a property as an Observable at a glance.

RxJS Operators with pipe()

import { map, catchError } from 'rxjs'; import { of } from 'rxjs'; getUsers(): Observable<User[]> { return this.http.get<User[]>(url).pipe( map((users) => users.filter((u) => u.name)), // transform the stream catchError((err) => of([])) // on error, emit an empty array instead ); }

An Observable's .pipe(...) applies operators — functions that transform the stream — much like chaining array methods, but for values arriving over time. map transforms each emitted value (here filtering the user list), and catchError handles failures, returning a fallback Observable (of([]) emits a single empty array). This is RxJS's equivalent of the try/catch error handling from React's Intermediate Chapter 6, expressed as a pipeline.

This is why reactive forms felt powerful
Chapter 9's valueChanges Observable plus these operators is exactly what enables debounced, live-reacting forms: searchControl.valueChanges.pipe(debounceTime(300), switchMap(term => this.api.search(term))) is the entire debounced-search-with-cancellation pattern that took React a custom useDebounce hook plus careful race-condition handling (Projects 4 and Intermediate Ch 6) — here it's a few composed operators.
Manual subscriptions need manual cleanup
A manual .subscribe() (option A) that isn't unsubscribed can leak memory and keep running after a component is gone — the same category of issue as a React useEffect without a cleanup function (Fundamentals Chapter 8). The async pipe sidesteps this entirely by cleaning up for you, which is the main reason to prefer it. When a manual subscription is genuinely needed, unsubscribe in ngOnDestroy (next chapter) — HTTP requests are a partial exception, as they complete after one emission, but it's safest to treat all subscriptions as needing cleanup.
AngularReact equivalent
http.get<T>(url)fetch(url).then(r => r.json())
Returns an ObservableReturns a Promise
async pipe in templateManual loading/data state in useState
.pipe(map, catchError)Transforms + try/catch
debounceTime + switchMapCustom useDebounce + race handling

Coding Challenges

Challenge 1

Set up provideHttpClient, build a service that fetches a list from a free public API returning an Observable, and a component that subscribes in ngOnInit and stores the result for display.

📄 View solution
Challenge 2

Rewrite Challenge 1's component to use the async pipe instead of a manual subscription — exposing the Observable directly (users$) and consuming it in the template with @if (users$ | async; as users).

📄 View solution
Challenge 3

Add .pipe() to the service method with a map operator transforming the data (e.g. extracting just the names) and a catchError operator returning an empty array on failure, then display the transformed result.

📄 View solution

Chapter 11 Quick Reference

  • HttpClient returns an Observable, not a Promise — provide it with provideHttpClient()
  • An Observable is lazy: it does nothing until subscribed; it can emit many values over time
  • API calls belong in a service (Chapter 6); the service returns the Observable unsubscribed
  • async pipe — subscribes, renders the value, and unsubscribes automatically (the preferred way)
  • .pipe(map, catchError, ...) — RxJS operators transforming the stream
  • Convention: an Observable property ends in $ (e.g. users$)
  • Manual subscriptions need cleanup (ngOnDestroy) — the async pipe avoids that entirely
  • Next chapter: lifecycle hooks — ngOnInit, ngOnChanges, ngOnDestroy