Suggest an editImprove this articleRefine the answer for “HttpClient and interceptors in Angular”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**`HttpClient`** is Angular's typed HTTP service that returns RxJS Observables for every request. Interceptors are middleware that transform every request and response globally. ```typescript this.http.get<User[]>('/api/users'); // HttpClient: one specific call export const authInterceptor: HttpInterceptorFn = (req, next) => next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })); // Interceptor: runs automatically on every request ``` **Key point:** Use `HttpClient` for individual API calls; use interceptors for auth tokens, logging, and global error handling.Shown above the full answer for quick recall.Answer (EN)Image**`HttpClient`** is Angular's typed HTTP service that wraps every request in an RxJS Observable. **Interceptors** are middleware functions that sit in front of every request and response, letting you transform them globally without touching individual service calls. ## Theory ### TL;DR - `HttpClient` analogy: a post office that handles serialization and delivery - you hand it a request, it handles the rest - Interceptor analogy: a security checkpoint every package passes through - inspect, modify, or reject - Main difference: `HttpClient` makes calls; interceptors modify those calls globally - Use interceptors for cross-cutting concerns: auth tokens, logging, global error handling - Interceptors run in registration order; each can short-circuit the chain by not calling `next()` ### Quick example ```typescript // HttpClient: typed GET request const users$ = this.http.get<User[]>('/api/users'); // Functional interceptor: adds auth token to EVERY request automatically export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = inject(AuthService).getToken(); if (!token) return next(req); const cloned = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); return next(cloned); // pass modified request forward }; // Result: every HTTP call gets the token with zero repetition in services ``` `HttpRequest` is immutable by design. Always use `clone()` to create a modified copy. ### Key difference `HttpClient` is the request maker. You call `.get()`, `.post()`, `.put()` and get an Observable back. Interceptors are request modifiers - they process the Observable before bytes leave your machine and again when the response arrives. Stack multiple interceptors and they run in sequence. The response flows back through the same chain in reverse. ### When to use - **`HttpClient` directly**: any specific HTTP call in a service or component - **Auth interceptor**: attach JWT tokens once, not in every service method - **Error interceptor**: catch 401/403 globally and redirect to login - **Logging interceptor**: track request timing and failures across the entire app - **Caching interceptor**: return stored GET responses without hitting the network - **Retry interceptor**: automatically retry failed requests with a delay ### Comparison table | Aspect | `HttpClient` | Interceptor | |---|---|---| | Purpose | Makes HTTP calls | Transforms requests/responses globally | | Scope | One request at a time | All requests matching your conditions | | Runs when | You call `.get()`, `.post()`, etc. | Before every request, after every response | | Can modify request | Yes, per call via options | Yes, for all calls at once | | Can short-circuit | No | Yes - skip `next()` to cancel or serve cached data | | Typical use | API calls in services | Auth, logging, errors, caching, retry | ### How the interceptor chain works When you call `this.http.get()`, Angular creates an `HttpRequest` and passes it through each registered interceptor in order. Each interceptor can clone and modify the request, then calls `next(cloned)` to forward it. The last handler actually sends the request to the network. The response then travels back through the same chain in reverse. If an interceptor skips `next()`, the chain stops and no request is sent. That is how caching interceptors return stored data without touching the network. Modern Angular (v15+) uses functional interceptors registered via `withInterceptors()`. Class-based interceptors with `implements HttpInterceptor` still work but are not the recommended pattern anymore. ```typescript // main.ts - registering interceptors bootstrapApplication(AppComponent, { providers: [ provideHttpClient( withInterceptors([ authInterceptor, // runs first loggingInterceptor, // runs second errorInterceptor, // runs third ]) ), ], }); ``` ### Common mistakes **Forgetting to return `next()` - the request never sends** ```typescript // WRONG: clones the request but never forwards it export const authInterceptor: HttpInterceptorFn = (req, next) => { const cloned = req.clone({ setHeaders: { 'X-Custom': 'value' } }); // Missing: return next(cloned); }; // RIGHT export const authInterceptor: HttpInterceptorFn = (req, next) => { const cloned = req.clone({ setHeaders: { 'X-Custom': 'value' } }); return next(cloned); }; ``` **Mutating the request instead of cloning it** ```typescript // WRONG: HttpRequest is immutable, this has no effect export const authInterceptor: HttpInterceptorFn = (req, next) => { req.headers.set('Authorization', 'Bearer token'); // no-op return next(req); // headers unchanged }; // RIGHT: clone() creates a new request with the changes applied export const authInterceptor: HttpInterceptorFn = (req, next) => { const cloned = req.clone({ setHeaders: { Authorization: 'Bearer token' } }); return next(cloned); }; ``` **Swallowing errors instead of re-throwing them** ```typescript // WRONG: component never knows the request failed export const errorInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( catchError(() => of(null)) // error disappears ); }; // RIGHT: handle what you need, then re-throw export const errorInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) inject(Router).navigate(['/login']); return throwError(() => error); }) ); }; ``` **Registering class-based interceptors in lazy-loaded modules** ```typescript // WRONG: if SharedModule is imported in multiple places, // the interceptor runs multiple times per request @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ] }) export class SharedModule {} // RIGHT with modern Angular: use functional interceptors // registered once in provideHttpClient(withInterceptors([...])) ``` **Creating infinite retry loops** ```typescript // WRONG: retrying a 401 without refreshing the token loops forever export const retryInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( retry(3) // retries blindly including 401s that will never succeed ); }; // RIGHT: limit retries and be specific about which errors warrant them export const retryInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( retry({ count: 3, delay: 1000 }) ); }; ``` ### Real-world usage - **NgRx apps**: interceptors dispatch loading/success/error actions so the store reflects HTTP state automatically - **Firebase**: interceptors attach ID tokens and refresh them when they expire - **Multi-environment APIs**: interceptors prepend base URLs or version segments like `/v2/` without touching service code - **Sentry or Datadog**: interceptors capture HTTP errors and send them to monitoring with full request context - **Response normalization**: interceptors convert `snake_case` from the backend to `camelCase` for the frontend In practice, the most common setup is two separate interceptors: one for auth, one for errors. Keeping them split makes each one easier to test and debug independently. ### Follow-up questions **Q:** Why does `HttpClient` return Observables instead of Promises? **A:** Observables are cancellable - unsubscribing stops the in-flight request. A Promise resolves once and cannot be cancelled. With Observables you also get operators like `retry()`, `debounceTime()`, and `shareReplay()` that make HTTP handling far more flexible. **Q:** What happens when one interceptor in the chain throws an error? **A:** The error propagates through each subsequent interceptor's `catchError` handler. If none catches it, it reaches the component's subscription. Order matters: register error-handling interceptors last if you want them to catch errors from all the others. **Q:** How do you skip an interceptor for specific requests? **A:** Add a custom header when making the request, then check for it inside the interceptor. Example: `req.clone({ setHeaders: { 'X-Skip-Auth': 'true' } })`. In the interceptor: `if (req.headers.has('X-Skip-Auth')) return next(req);` **Q:** Can interceptors modify response bodies? **A:** Yes. Use `map()` after `next()` and filter for `HttpResponse` events: `map(event => event instanceof HttpResponse ? event.clone({ body: transform(event.body) }) : event)`. Streaming responses like file downloads need separate handling since the full body is not available as a single event. **Q:** (Senior) How would you implement request deduplication - preventing duplicate calls to the same endpoint within 100ms? **A:** Keep a `Map<string, Observable>` of pending requests keyed by URL. When the same URL arrives within the time window, return the cached Observable using `shareReplay(1)` instead of making a new call. Clear the entry once the request completes or errors out. ## Examples ### Basic: HttpClient with typed requests and error handling ```typescript @Injectable({ providedIn: 'root' }) export class UserService { private http = inject(HttpClient); getUsers(): Observable<User[]> { return this.http.get<User[]>('/api/users').pipe( retry(2), catchError((error: HttpErrorResponse) => { if (error.status === 404) return of([]); return throwError(() => error); }) ); } createUser(user: CreateUserDto): Observable<User> { return this.http.post<User>('/api/users', user); } searchUsers(query: string): Observable<User[]> { return this.http.get<User[]>('/api/users', { params: { q: query, limit: '10' } }); } } // HttpClient handles JSON serialization automatically. // The <User[]> generic gives you type safety on the response body. ``` The `retry(2)` retries the request twice before giving up. For 404 responses the service returns an empty array instead of throwing, which is a common pattern when a missing resource is not an error state for the UI. ### Intermediate: Auth interceptor with token refresh ```typescript export const authInterceptor: HttpInterceptorFn = (req, next) => { const auth = inject(AuthService); const router = inject(Router); // Skip token for login endpoint if (req.url.includes('/auth/login')) return next(req); const token = auth.getToken(); if (!token) { router.navigate(['/login']); return throwError(() => new Error('No token')); } const cloned = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); return next(cloned).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) { // Token expired - try to refresh once return auth.refreshToken().pipe( switchMap((newToken) => { const retried = req.clone({ setHeaders: { Authorization: `Bearer ${newToken}` } }); return next(retried); }), catchError(() => { router.navigate(['/login']); return throwError(() => error); }) ); } return throwError(() => error); }) ); }; // The component making the request never knows a refresh happened. // Token rotation is transparent to the rest of the app. ``` ### Advanced: Caching interceptor with TTL ```typescript @Injectable({ providedIn: 'root' }) export class HttpCacheService { private cache = new Map<string, { body: any; timestamp: number }>(); private readonly TTL = 5 * 60 * 1000; // 5 minutes get(url: string): any | null { const entry = this.cache.get(url); if (!entry || Date.now() - entry.timestamp > this.TTL) return null; return entry.body; } set(url: string, body: any): void { this.cache.set(url, { body, timestamp: Date.now() }); } } export const cacheInterceptor: HttpInterceptorFn = (req, next) => { const cache = inject(HttpCacheService); if (req.method !== 'GET') return next(req); const cached = cache.get(req.url); if (cached) { return of(new HttpResponse({ body: cached, status: 200 })); } return next(req).pipe( tap((event) => { if (event instanceof HttpResponse) { cache.set(req.url, event.body); } }) ); }; // For cached URLs the chain stops here - next() is never called. // No network request is made for fresh cache entries. ``` This interceptor short-circuits the chain for GET requests with valid cache entries. The component receives an `HttpResponse` object identical in shape to a real response, so no special handling is needed on the calling side.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.