Back to Blog
Angular20 min read

Angular HTTP Client and Interceptors: Complete Guide

Angular HTTP Client provides a powerful API for making HTTP requests. Learn how to use HttpClient, create interceptors for authentication, error handling, caching, and request/response transformation in enterprise Angular applications.

Making HTTP requests in Angular is straightforward, but handling authentication tokens, error responses, loading states, and request transformations across an entire application can become tedious. That's where HTTP Interceptors come in. They're one of my favorite Angular features because they let you handle all this cross-cutting concerns in one place.

Angular HTTP Client is built on RxJS Observables, which makes it perfect for handling asynchronous operations. Interceptors are middleware that sit between your application and the HTTP backend. They can modify requests (add headers, transform data), handle responses (transform data, catch errors), and implement features like authentication, logging, and caching globally.

In this guide, I'll walk you through building HTTP services and interceptors the way I do in production applications. We'll cover basic HTTP service setup, authentication interceptors (adding tokens automatically), error handling interceptors (catching and handling errors globally), request/response transformation, loading state management, and caching strategies. I'll also share some patterns I've learned for organizing interceptors and handling edge cases.

Basic HTTP Service

Create a service using HttpClient:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class BusinessService {
  private url: string;

  constructor(private http: HttpClient) {
    this.url = `${environment.ApiUrl}business`;
  }

  public GetBusinesses(data: any): Observable<any> {
    return this.http.post(`${this.url}/get`, data);
  }

  public GetBusiness(businessId: number): Observable<any> {
    return this.http.get(`${this.url}/${businessId}/details`);
  }

  public SaveBusiness(data: any, businessId: number): Observable<any> {
    return this.http.post(`${this.url}/${businessId}/details`, data);
  }

  public DeleteBusiness(id: number): Observable<any> {
    return this.http.delete(`${this.url}/${id}`);
  }
}

Authentication Interceptor

Add authentication headers automatically:

import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authToken = this.authService.getToken();
    
    if (authToken) {
      const cloned = req.clone({
        setHeaders: {
          Authorization: `Bearer ${authToken}`
        }
      });
      return next.handle(cloned);
    }
    
    return next.handle(req);
  }
}

// Register in app.module.ts
providers: [
  {
    provide: HTTP_INTERCEPTORS,
    useClass: AuthInterceptor,
    multi: true
  }
]

Error Handling Interceptor

Handle errors globally:

import { Injectable } from '@angular/core';
import { HttpEvent, HttpErrorResponse, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Router } from '@angular/router';
import { ToastService } from './toast.service';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor(
    private router: Router,
    private toast: ToastService
  ) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        let errorMessage = 'An error occurred';
        
        if (error.error instanceof ErrorEvent) {
          // Client-side error
          errorMessage = `Error: ${error.error.message}`;
        } else {
          // Server-side error
          switch (error.status) {
            case 401:
              errorMessage = 'Unauthorized';
              this.router.navigate(['/login']);
              break;
            case 403:
              errorMessage = 'Forbidden';
              break;
            case 404:
              errorMessage = 'Not Found';
              break;
            case 500:
              errorMessage = 'Internal Server Error';
              break;
            default:
              errorMessage = `Error Code: ${error.status}
Message: ${error.message}`;
          }
        }
        
        this.toast.error(errorMessage);
        return throwError(() => error);
      })
    );
  }
}

Caching Interceptor

Implement response caching:

import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { filter, share, tap } from 'rxjs/operators';
import { CacheService } from './cache.service';

@Injectable()
export class CacheInterceptor implements HttpInterceptor {
  constructor(private cacheService: CacheService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (req.method !== 'GET') {
      return next.handle(req);
    }

    if (this.canCache(req)) {
      const cachedResponse = this.cacheService.getWithExpiry(req.urlWithParams);
      
      if (cachedResponse === null) {
        return next.handle(req).pipe(
          filter(event => event instanceof HttpResponse),
          tap((event: HttpResponse<any>) => {
            this.cacheService.setWithExpiry(req.urlWithParams, event.body);
          }),
          share()
        );
      } else {
        return of(new HttpResponse({ body: cachedResponse }));
      }
    }
    
    return next.handle(req);
  }

  private canCache(req: HttpRequest<any>): boolean {
    // Define cacheable URLs
    const cacheableUrls = ['/api/lookup', '/api/config'];
    return cacheableUrls.some(url => req.url.startsWith(url));
  }
}

Request/Response Transformation

Transform data in interceptors:

import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Transform request
    const transformedReq = req.clone({
      body: this.transformRequest(req.body)
    });

    return next.handle(transformedReq).pipe(
      map(event => {
        if (event instanceof HttpResponse) {
          // Transform response
          return event.clone({
            body: this.transformResponse(event.body)
          });
        }
        return event;
      })
    );
  }

  private transformRequest(body: any): any {
    // Add timestamp, format data, etc.
    if (body) {
      return { ...body, timestamp: Date.now() };
    }
    return body;
  }

  private transformResponse(body: any): any {
    // Unwrap API response, transform data structure
    if (body && body.data) {
      return body.data;
    }
    return body;
  }
}

Error Handling in Services

Handle errors in service methods:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  constructor(private http: HttpClient) {}

  getData(): Observable<any> {
    return this.http.get('/api/data').pipe(
      retry(3), // Retry failed requests up to 3 times
      catchError(error => {
        console.error('Error fetching data:', error);
        return throwError(() => new Error('Failed to fetch data'));
      })
    );
  }

  searchData(term: string): Observable<any> {
    if (!term || term.length < 3) {
      return throwError(() => new Error('Search term too short'));
    }
    
    return this.http.post('/api/search', { term }).pipe(
      catchError(error => {
        if (error.status === 404) {
          return throwError(() => new Error('No results found'));
        }
        return throwError(() => error);
      })
    );
  }
}

Best Practices

  • Use interceptors for cross-cutting concerns like authentication and error handling
  • Implement proper error handling with user-friendly messages
  • Use caching interceptors for GET requests to improve performance
  • Handle loading states appropriately in components
  • Use RxJS operators for request/response transformation
  • Implement retry logic for transient failures
  • Use typed responses with interfaces for better type safety
  • Handle different HTTP status codes appropriately
  • Log errors for debugging while showing user-friendly messages
  • Use environment variables for API URLs

Conclusion

Angular HTTP Client and Interceptors provide a powerful foundation for building robust HTTP services. With proper error handling, authentication, caching, and transformation, you can create maintainable and scalable API integration layers for enterprise Angular applications.