Learn Angular - API Communication & Caching
Episode 14 of 24

Learn Angular - API Communication & Caching

This episode covers API communication and caching: HTTP interceptors for centralized error handling, caching strategies for API responses, state transfer and server-side rendering cache, and optimizing network request performance.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

In episode 8 you learned the HTTP basics. Now it's time to make your API communication professional: centralized error handling, smart caching, and lightweight requests.

Episode 14 covers HTTP interceptors for centralized error handling, caching strategies for API responses, state transfer and SSR cache, and optimizing network request performance. This is the bridge from an application that works to an application that's resilient.

HTTP Interceptors and Centralized Error Handling

An Interceptor for Logging and Errors

An interceptor catches every HTTP request and response, making it the ideal place for cross-cutting logic like error handling. The functional form has been available since Angular 17:

JSCentralized error-handling interceptor
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { NotifService } from './notif.service';
 
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const notif = inject(NotifService);
 
  return next(req).pipe(
    catchError((err: HttpErrorResponse) => {
      if (err.status === 401) {
        notif.tampil('Sesi habis, silakan login kembali', 'error');
      } else if (err.status >= 500) {
        notif.tampil('Terjadi kesalahan server', 'error');
      }
      console.error(`[${err.status}] ${req.method} ${req.url}`);
      return throwError(() => err);
    }),
  );
};

errorInterceptor handles errors from all requests in one place: status 401 tells the user the session has expired, status 5xx shows a server message. No more try-catch in every service — just register the interceptor with provideHttpClient(withInterceptors([errorInterceptor])).

Interceptor Order

Interceptors run in registration order for requests and in reverse for responses. The auth interceptor should be registered before the error interceptor so requests always carry the token when errors are computed.

Caching Strategies for API Responses

Caching at the Stream Level

Caching prevents repeated requests for the same data. The simplest RxJS approach is shareReplay as in episode 8, but you need a refresh policy:

JSSimple TTL cache
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, shareReplay, tap, timer, switchMap } from 'rxjs';
 
@Injectable({ providedIn: 'root' })
export class KatalogService {
  private readonly http = inject(HttpClient);
  private cache$: Observable<Produk[]> | null = null;
  private readonly ttl = 300000;
 
  listar(): Observable<Produk[]> {
    if (!this.cache$) {
      this.cache$ = this.http.get<Produk[]>('/api/produk').pipe(
        shareReplay(1),
        switchMap((data) =>
          timer(this.ttl).pipe(switchMap(() => this.cache$ = null)),
        ),
      );
    }
    return this.cache$;
  }
}

After five minutes (TTL of 300000 milliseconds), cache$ is set to null so the next request runs again. This strikes a balance between responsiveness and data freshness.

Cache Invalidation

A cache must be invalidatable when data changes. A common method: an invalidate() method that sets cache$ = null, called after create, update, or delete operations. For frequently changing data, consider a tag- or key-based cache backed by a dedicated library.

State Transfer and Server-side Rendering Cache

State Transfer with TransferState

When a page is rendered on the server (SSR), the same data can be transferred directly to the browser so the client doesn't need to request it again. Angular provides TransferState:

JSTransferState for SSR
import { Injectable, inject, TransferState, makeStateKey } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { of, tap } from 'rxjs';
 
const PRODUK_KEY = makeStateKey<Produk[]>('produk-lengkap');
 
@Injectable({ providedIn: 'root' })
export class ProdukService {
  private readonly http = inject(HttpClient);
  private readonly state = inject(TransferState);
 
  listar() {
    const cached = this.state.get(PRODUK_KEY, null);
    if (cached) {
      return of(cached);
    }
    return this.http.get<Produk[]>('/api/produk').pipe(
      tap((data) => this.state.set(PRODUK_KEY, data)),
    );
  }
}

On the server, data is fetched and stored in TransferState. In the browser, the value is read directly from state, so there's no second request. This eliminates the delay and reduces API load for the same page.

SSR Cache

The SSR render output itself can be cached on a CDN or server. Public pages with rarely changing data can be stored as static cache with a TTL, while personalized pages are still rendered per request.

Network Request Performance

Combining and Parallelizing

Reduce the number of round-trips: combine independent requests with forkJoin, and cancel requests that are no longer relevant with switchMap. Send smaller payloads — request only the fields you need via query params or sparse fields.

JSCombining parallel requests
import { forkJoin } from 'rxjs';
 
forkJoin({
  profil: this.http.get<Profil>('/api/profil'),
  order: this.http.get<Order[]>('/api/order?limit=10'),
}).subscribe(({ profil, order }) => {
  this.profil.set(profil);
  this.order.set(order);
});

forkJoin waits for all requests to finish, then returns a single combined object — one subscription for two parallel requests. This cuts the waiting time compared to loading them one by one.

Measuring and Observing

Always measure: monitor the number of requests per page, payload size, and response time on slow networks. Use HttpClient with withFetch for fetch API performance, and inspect the network waterfall in DevTools to find requests that are wasteful or sequential when they could be parallel.

Wrap Up

Key takeaways:

  • An interceptor is one place for centralized error handling and logging.
  • shareReplay plus a TTL gives you simple cache with automatic refresh.
  • Provide an invalidate method to clear the cache when data changes.
  • TransferState moves data from server to browser so SSR has no double requests.
  • Cache SSR render output on the CDN for public pages that rarely change.
  • Combine parallel requests with forkJoin and measure performance regularly.

In the next episode, episode 15, we'll cover performance optimization — profiling with Angular DevTools, the Default versus OnPush change detection strategies, trackBy and lazy loading for code splitting, and bundle analysis and production optimization.

Learn Angular - API Communication & Caching | Learn Angular