Skip to main content
Angular HTTP Client

Module Overview

Estimated Time: 4-5 hours | Difficulty: Intermediate | Prerequisites: Module 7
Angular’s HttpClient provides a powerful API for making HTTP requests while RxJS enables reactive data handling. Together they form the foundation of Angular’s data layer. What You’ll Learn:
  • HttpClient setup and configuration
  • Making HTTP requests (GET, POST, PUT, DELETE)
  • HTTP interceptors for cross-cutting concerns
  • Error handling strategies
  • RxJS operators for data transformation
  • Caching and request management

HttpClient Setup


Making HTTP Requests

Basic CRUD Operations

Request Options


HTTP Interceptors

Interceptors handle cross-cutting concerns — tasks that apply to every HTTP request or response, regardless of which component or service initiated them. Without interceptors, you would need to manually add auth tokens to every API call, log every request individually, and handle 401 errors in every service method. Interceptors let you write that logic once and have it apply globally. Think of interceptors like airport security checkpoints: every passenger (request) passes through the same screening process regardless of their destination.

Auth Interceptor

Error Interceptor

Logging Interceptor

Caching Interceptor


RxJS Operators

Common Operators for HTTP

SwitchMap vs MergeMap vs ConcatMap vs ExhaustMap


Error Handling Patterns

Service Level Error Handling

Component Level Error Handling


Cancellation and Cleanup


Signals with HTTP

Using toSignal

Reactive Data Loading


Practice Exercise

Exercise: Build a Data Table with Pagination and Search

Create a data table component that:
  1. Fetches paginated data from API
  2. Has search with debounce
  3. Supports sorting
  4. Shows loading and error states
  5. Uses proper RxJS operators

Summary

1

HttpClient

Use provideHttpClient() and inject HttpClient for API calls
2

Interceptors

Handle auth, logging, errors, and caching globally
3

RxJS Operators

Use switchMap for search, catchError for errors, shareReplay for caching
4

Error Handling

Handle errors at service and component levels with retry logic
5

Signals Integration

Use toSignal/toObservable for seamless reactive integration

Interview Deep-Dive

Strong Answer: The naive approach — catching the 401, calling refreshToken, then retrying — breaks when three requests fail at the same time. All three independently trigger refreshToken, causing three refresh calls where only one is needed.The fix is a shared refresh observable. I maintain a BehaviorSubject or a class-level variable that tracks whether a refresh is already in progress. When the first 401 arrives, the interceptor starts the refresh and stores the observable (using shareReplay(1) so late subscribers get the same result). When the second and third 401s arrive, they check the in-progress flag and subscribe to the existing refresh observable instead of starting a new one.The implementation: the interceptor catches 401 errors. If no refresh is in progress, it sets a flag, calls authService.refreshToken().pipe(shareReplay(1)), stores the result, and then switchMaps to retry the original request with the new token. If a refresh IS in progress, it waits for the existing refresh to complete, then retries with the new token. After the refresh completes (success or failure), it clears the flag.Edge case: if the refresh token itself is expired, the refresh call returns 401. You must NOT retry the refresh in an infinite loop. I detect this by checking if the failing request IS the refresh endpoint, and if so, I log out the user instead of retrying.Follow-up: How do you handle requests that were queued while the token was refreshing? Answer: Each 401 request’s handler subscribes to the shared refresh observable. When it emits the new token, all waiting handlers simultaneously retry their original requests with the updated token. Because they all share the same observable via shareReplay, they all proceed at once after a single refresh call completes. The requests queue themselves naturally via the RxJS subscription mechanism — no manual queue needed.
Strong Answer: These four operators all map a source emission to an inner observable, but they differ in how they handle overlapping inner subscriptions.switchMap cancels the previous inner observable when a new source emission arrives. Use for search/autocomplete where only the latest result matters. mergeMap subscribes to all inner observables in parallel with no cancellation. Use for fire-and-forget operations like analytics events. concatMap queues inner observables and processes them sequentially. Use when order matters, like sequential file uploads. exhaustMap ignores new source emissions while an inner observable is in progress. Use for form submissions.For form submissions, exhaustMap is the right choice. Here is why: if the user clicks “Submit” three times quickly, switchMap would cancel the first two submissions (potentially leaving the server in an inconsistent state with a half-processed order). mergeMap would send three identical submissions in parallel (creating three orders). concatMap would queue all three and process them sequentially (still creating three orders, just slower). exhaustMap ignores the second and third clicks because the first submission is still in progress. Exactly one submission goes through.The common mistake I see is developers using switchMap for submissions because they are used to it from search. switchMap for POST requests is dangerous — the cancelled request might have already been processed server-side, but the client discards the response.Follow-up: What if you need to show a “submitting…” state during the exhaustMap? Answer: I use a signal isSubmitting = signal(false), set it to true before the exhaustMap inner observable, and false in a finalize operator. Because exhaustMap ignores new emissions during processing, the button can be disabled based on isSubmitting(), providing visual feedback that matches the actual behavior. Alternatively, I disable the submit button with [disabled]=“isSubmitting()” as a UX-level protection on top of the exhaustMap-level protection.
Strong Answer: There are three levels of HTTP caching in Angular. The simplest is shareReplay(1) on the service’s observable — multiple subscribers share one HTTP call, and late subscribers get the cached result. This is a per-session, in-memory cache.For more control, I implement a caching interceptor that checks a Map before forwarding requests. GET requests are cached by URL (including query params). Non-GET requests (POST, PUT, DELETE) invalidate related cache entries. The interceptor can also respect custom headers (like X-Cache-TTL) to control per-endpoint cache duration.For persistent caching across sessions, I use the service worker’s caching strategy (via Angular PWA) or manually cache to IndexedDB. The service worker approach is declarative — you configure it in ngsw-config.json — but it caches at the HTTP level without awareness of your application logic.Invalidation strategies: time-based (TTL — cache expires after N seconds), event-based (invalidate the users cache when a user is created/updated), and manual (expose a cache.clear() method for pull-to-refresh). In practice, I combine all three: a 5-minute TTL for list endpoints, immediate invalidation on mutations, and a manual refresh button.The gotcha with shareReplay: if the source observable errors, shareReplay replays the error to all new subscribers. You need catchError before shareReplay, or use a pattern that retries on error and only caches successes.Follow-up: How does Angular’s built-in HTTP transfer cache (for SSR) relate to this? Answer: Angular’s withHttpTransferCacheOptions in provideClientHydration automatically transfers HTTP responses made during server-side rendering to the client. The client-side HttpClient checks the transfer state before making a network call. This is a one-time, SSR-to-client cache — it prevents duplicate requests during hydration but does not provide ongoing caching. For that, you still need the interceptor-based or shareReplay approach.

Next Steps

Next: RxJS Deep Dive

Master advanced RxJS patterns for complex async scenarios