Angular: Zero to Senior · Lesson 7 of 11
HTTP Client & RxJS Essentials
Angular uses RxJS Observables for async operations — including HTTP calls. Understanding both HttpClient and the core RxJS operators unlocks clean, composable async code.
Setup
Register HttpClient in app.config.ts:
TYPESCRIPT
import { provideHttpClient, withInterceptors } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
],
};HttpClient Basics
TYPESCRIPT
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { inject } from '@angular/core';
export interface Product {
id: number;
name: string;
price: number;
}
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
private baseUrl = '/api/products';
getAll(page = 1, pageSize = 20): Observable<Product[]> {
const params = new HttpParams()
.set('page', page)
.set('pageSize', pageSize);
return this.http.get<Product[]>(this.baseUrl, { params });
}
getById(id: number): Observable<Product> {
return this.http.get<Product>(`${this.baseUrl}/${id}`);
}
create(product: Omit<Product, 'id'>): Observable<Product> {
return this.http.post<Product>(this.baseUrl, product);
}
update(id: number, changes: Partial<Product>): Observable<Product> {
return this.http.patch<Product>(`${this.baseUrl}/${id}`, changes);
}
delete(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}`);
}
}Subscribing in Components
With async pipe (preferred)
TYPESCRIPT
@Component({
standalone: true,
imports: [AsyncPipe, CommonModule],
template: `
@if (products$ | async; as products) {
@for (p of products; track p.id) {
<p>{{ p.name }} — {{ p.price | currency }}</p>
}
} @else {
<p>Loading...</p>
}
`,
})
export class ProductListComponent {
products$ = inject(ProductService).getAll();
}The async pipe subscribes and automatically unsubscribes when the component is destroyed.
With subscribe() (when you need side effects)
TYPESCRIPT
@Component({ /* ... */ })
export class ProductDetailComponent implements OnInit, OnDestroy {
private sub!: Subscription;
product!: Product;
isLoading = true;
error = '';
ngOnInit(): void {
const id = Number(inject(ActivatedRoute).snapshot.paramMap.get('id'));
this.sub = inject(ProductService).getById(id).subscribe({
next: (p) => { this.product = p; this.isLoading = false; },
error: (err) => { this.error = err.message; this.isLoading = false; },
complete: () => { this.isLoading = false; },
});
}
ngOnDestroy(): void {
this.sub.unsubscribe();
}
}Error Handling
TYPESCRIPT
import { catchError, throwError } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
getAll(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products').pipe(
catchError(this.handleError),
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
let message = 'An error occurred';
if (error.status === 0) {
message = 'Network error — check your connection';
} else if (error.status === 401) {
message = 'Unauthorized — please log in again';
} else if (error.status === 404) {
message = 'Resource not found';
} else if (error.status >= 500) {
message = 'Server error — please try again later';
} else {
message = error.error?.message ?? error.message;
}
return throwError(() => new Error(message));
}
}HTTP Interceptors
Interceptors run on every request and response — perfect for auth tokens, logging, and error handling globally.
Auth Token Interceptor
TYPESCRIPT
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
if (token) {
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`),
});
return next(authReq);
}
return next(req);
};Register interceptors:
TYPESCRIPT
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([authInterceptor])),
],
};Retry Interceptor
TYPESCRIPT
import { HttpInterceptorFn } from '@angular/common/http';
import { retry, timer } from 'rxjs';
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
retry({
count: 3,
delay: (error, retryCount) => timer(retryCount * 1000), // 1s, 2s, 3s
}),
);
};Essential RxJS Operators
Transformation
TYPESCRIPT
import { map, switchMap, mergeMap, concatMap, exhaustMap } from 'rxjs/operators';
// map — transform each emitted value
users$.pipe(
map(users => users.filter(u => u.active)),
)
// switchMap — cancel previous inner observable on new emission
// Perfect for search-as-you-type (cancels in-flight request)
searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.productService.search(term)),
)
// mergeMap — run all inner observables concurrently
// Good for parallel API calls
userIds$.pipe(
mergeMap(id => this.userService.getById(id)),
)
// concatMap — wait for each inner observable to complete before starting next
// Good for sequential operations
orderIds$.pipe(
concatMap(id => this.orderService.process(id)),
)
// exhaustMap — ignore new emissions while inner observable is running
// Perfect for submit buttons (ignores double-clicks)
submitClicks$.pipe(
exhaustMap(() => this.orderService.submit(this.form.value)),
)Filtering
TYPESCRIPT
import { filter, take, takeUntil, debounceTime, distinctUntilChanged } from 'rxjs/operators';
// filter — only pass values that satisfy a predicate
users$.pipe(filter(u => u.age >= 18))
// debounceTime — wait for a pause in emissions (search inputs)
input$.pipe(debounceTime(300))
// distinctUntilChanged — skip if value is same as last emission
input$.pipe(distinctUntilChanged())
// take — complete after N emissions
timer(0, 1000).pipe(take(5)) // emit 5 times, then complete
// takeUntil — complete when a notifier emits (great for unsubscribing)
this.http.get('/api/data').pipe(
takeUntil(this.destroy$),
)Combination
TYPESCRIPT
import { combineLatest, forkJoin, zip } from 'rxjs';
// forkJoin — wait for all to complete, emit last value of each
// Perfect for parallel API calls where you need all results
forkJoin({
user: this.userService.getById(id),
orders: this.orderService.getByUserId(id),
products: this.productService.getFeatured(),
}).subscribe(({ user, orders, products }) => {
// all three available together
});
// combineLatest — emit whenever any source emits, with latest from all
// Perfect for combining filters
combineLatest([search$, categoryFilter$, sortOrder$]).pipe(
debounceTime(100),
switchMap(([search, category, sort]) =>
this.productService.getAll({ search, category, sort })
),
)Error Handling
TYPESCRIPT
import { catchError, retry, retryWhen, throwError, EMPTY } from 'rxjs';
// catchError — handle error, return fallback or rethrow
products$.pipe(
catchError(err => {
console.error(err);
return of([]); // return empty array as fallback
// OR: return throwError(() => err); // rethrow
// OR: return EMPTY; // complete silently
}),
)
// retry — retry N times on error
http.get('/api/data').pipe(retry(3))takeUntilDestroyed — Modern Unsubscription (Angular 16+)
No need to implement OnDestroy manually:
TYPESCRIPT
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DestroyRef, inject } from '@angular/core';
@Component({ /* ... */ })
export class MyComponent {
private destroyRef = inject(DestroyRef);
ngOnInit(): void {
this.someService.data$.pipe(
takeUntilDestroyed(this.destroyRef),
).subscribe(data => this.data = data);
}
}Signals vs Observables
Angular 16+ introduced signals as a simpler reactive primitive:
TYPESCRIPT
// Observable approach
products$ = this.productService.getAll();
// Template: {{ products$ | async }}
// Signal approach (Angular 17+)
products = toSignal(this.productService.getAll(), { initialValue: [] });
// Template: {{ products() }} — no async pipe neededUse Observables for HTTP calls and event streams. Use signals for local component state and derived values.
Quick Reference
Setup: provideHttpClient() in app.config.ts
GET: http.get(url, { params })
POST: http.post(url, body)
PATCH/PUT: http.patch(url, body)
DELETE: http.delete(url)
Interceptors: HttpInterceptorFn → withInterceptors([fn])
Error: .pipe(catchError(err => throwError(() => err)))
Retry: .pipe(retry({ count: 3, delay: ... }))
Key operators:
switchMap — cancel on new (search-as-you-type)
exhaustMap — ignore while busy (submit button)
forkJoin — parallel calls, wait for all
combineLatest — reactive filters
debounceTime — pause before emitting
takeUntilDestroyed — auto-unsubscribe (Angular 16+)