This episode covers HTTP and data fetching in Angular: using HttpClient for REST API integration, working with Observables and RxJS, handling errors with retry, and caching and request optimization strategies.

Modern applications almost never stand alone — they need data from a server. Angular provides HttpClient for HTTP communication, built on top of RxJS Observables, which are the main async pattern throughout the framework.
Episode 8 covers REST API integration with HttpClient, working with Observables and RxJS operators, error handling with retry, and caching and request optimization. These are core skills for any application that talks to a backend.
HttpClient is enabled via provideHttpClient in app.config.ts:
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
],
};provideHttpClient(withFetch()) enables HttpClient and uses the modern fetch API as its engine. After this, services can inject HttpClient.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Post } from './post.model';
@Injectable({ providedIn: 'root' })
export class PostService {
private readonly http = inject(HttpClient);
private readonly apiUrl = 'https://jsonplaceholder.typicode.com';
listar(): Observable<Post[]> {
return this.http.get<Post[]>(`${this.apiUrl}/posts`);
}
cari(id: number): Observable<Post> {
return this.http.get<Post>(`${this.apiUrl}/posts/${id}`);
}
buat(data: Post): Observable<Post> {
return this.http.post<Post>(`${this.apiUrl}/posts`, data);
}
}this.http.get<Post[]>(url) returns an Observable<Post[]>. TypeScript generics here aren't a runtime guarantee, but they help you write correct code. post, put, delete, and patch follow the same pattern.
An Observable doesn't run until it's subscribed to. The common pattern in a component: call a service method, subscribe, then store the result.
import { Component, inject, signal, OnInit } from '@angular/core';
import { PostService } from './post.service';
@Component({
selector: 'app-post-list',
standalone: true,
template: `
@for (post of posts(); track post.id) {
<p>{{ post.title }}</p>
}
`,
})
export class PostListComponent implements OnInit {
private readonly postService = inject(PostService);
readonly posts = signal<Post[]>([]);
ngOnInit(): void {
this.postService.listar().subscribe({
next: (data) => this.posts.set(data),
error: (err) => console.error('Gagal memuat post', err),
});
}
}The HTTP result is stored in a signal so the template reacts to changes. Notice the error handler — without it, an HTTP error becomes an uncaught error in the console.
RxJS is a transformation pipeline. Some operators you'll use most often with HttpClient:
import { map, tap, catchError, retry, throwError, of } from 'rxjs';
this.http.get<Post[]>(url).pipe(
retry({ count: 3, delay: 1000 }),
map((posts) => posts.filter((p) => p.published)),
tap((posts) => console.log(`Dimuat ${posts.length} post`)),
catchError((err) => {
console.error(err);
return of([]);
}),
);retry({ count: 3, delay: 1000 }) retries the request three times with a one-second delay for transient errors like timeouts. map transforms the data, tap executes side effects, and catchError catches failures — here it returns an empty list so the UI doesn't crash.
An HTTP error isn't always the server's fault. HttpErrorResponse distinguishes client-side (network) and server-side (status code) errors:
import { HttpErrorResponse } from '@angular/common/http';
import { throwError } from 'rxjs';
this.http.get<Post>(url).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 404) {
console.error('Data tidak ditemukan');
return throwError(() => new Error('Post tidak ada'));
}
if (err.status >= 500) {
console.error('Kesalahan server, coba lagi nanti');
return throwError(() => err);
}
console.error('Masalah jaringan atau kesalahan lain', err.message);
return throwError(() => err);
}),
);err.status === 404 marks data not found, while status 500 marks a server problem that may be transient. throwError forwards the error to the subscriber so the UI can show an appropriate message.
Without caching, every component that needs the same data makes a repeated request. Use the shareReplay operator to store the result and share it:
import { shareReplay } from 'rxjs';
private readonly cache$ = this.http.get<Post[]>(url).pipe(shareReplay(1));
listar(): Observable<Post[]> {
return this.cache$;
}shareReplay(1) runs the request once, then stores the result and shares it with all subsequent subscribers. This is the simplest form of caching at the data-stream level.
A few optimization practices: use distinctUntilChanged to avoid duplicate requests, combine parallel requests with forkJoin or combineLatest, cancel unneeded requests with switchMap, and clean up subscriptions in ngOnDestroy. Deeper caching details, including cache invalidation and HTTP interceptors, will be covered in episode 14.
Key takeaways:
provideHttpClient enables HttpClient in a standalone application.HttpClient returns an Observable that only runs once subscribed.retry, map, tap, and catchError form a safe data pipeline.HttpErrorResponse.shareReplay provides simple caching for data that rarely changes.switchMap, forkJoin, and subscription cancellation.In the next episode, episode 9, we'll cover routing and navigation — using the Angular Router, arranging route configuration with child routes and lazy loading, protecting routes with guards and resolvers, and managing query params, fragments, and navigation extras.