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.
π Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.
What are Angular HTTP Interceptors?
Angular HTTP Interceptors provide:
- Request Interception - Modify requests before they're sent
- Response Interception - Transform responses before they reach components
- Error Handling - Catch and handle errors globally
- Authentication - Add tokens automatically to all requests
- Caching - Cache responses to improve performance
- Logging - Log all HTTP requests and responses
- Transformation - Transform request/response data
Setting Up HttpClient
First, import HttpClientModule in your Angular module:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserModule,
HttpClientModule
],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
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}`);
}
}
Typed HTTP Service
export interface Business {
id: number;
name: string;
description: string;
}
export interface BusinessResponse {
success: boolean;
data: Business[];
}
@Injectable({
providedIn: 'root'
})
export class BusinessService {
constructor(private http: HttpClient) {}
getBusinesses(): Observable<BusinessResponse> {
return this.http.get<BusinessResponse>('/api/businesses');
}
getBusiness(id: number): Observable<Business> {
return this.http.get<Business>(`/api/businesses/${id}`);
}
}
Authentication Interceptor
Add authentication headers automatically to all requests:
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 or app.config.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
Advanced Auth Interceptor with Token Refresh
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(
private authService: AuthService,
private router: Router
) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Skip auth for login/register endpoints
if (req.url.includes('/login') || req.url.includes('/register')) {
return next.handle(req);
}
const authToken = this.authService.getToken();
if (authToken) {
// Check if token is expired
if (this.authService.isTokenExpired()) {
// Try to refresh token
return this.authService.refreshToken().pipe(
switchMap(newToken => {
const cloned = req.clone({
setHeaders: {
Authorization: `Bearer ${newToken}`
}
});
return next.handle(cloned);
}),
catchError(error => {
// Refresh failed, redirect to login
this.router.navigate(['/login']);
return throwError(() => error);
})
);
}
const cloned = req.clone({
setHeaders: {
Authorization: `Bearer ${authToken}`
}
});
return next.handle(cloned);
}
return next.handle(req);
}
}
Error Handling Interceptor
Handle errors globally with an error interceptor:
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 - Please login again';
this.authService.logout();
this.router.navigate(['/login']);
break;
case 403:
errorMessage = 'Forbidden - You do not have permission';
break;
case 404:
errorMessage = 'Resource not found';
break;
case 500:
errorMessage = 'Internal server error - Please try again later';
break;
case 0:
errorMessage = 'Network error - Please check your connection';
break;
default:
errorMessage = error.error?.message || `Error Code: ${error.status}`;
}
}
this.toast.error(errorMessage);
return throwError(() => error);
})
);
}
}
Error Interceptor with Retry Logic
import { retry, catchError, retryWhen, delay, take } from 'rxjs/operators';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
retryWhen(errors =>
errors.pipe(
delay(1000),
take(3),
catchError(error => throwError(() => error))
)
),
catchError((error: HttpErrorResponse) => {
// Handle error
return throwError(() => error);
})
);
}
}
Caching Interceptor
Implement response caching for GET requests:
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>> {
// Only cache GET requests
if (req.method !== 'GET') {
return next.handle(req);
}
if (this.canCache(req)) {
const cachedResponse = this.cacheService.getWithExpiry(req.urlWithParams);
if (cachedResponse !== null) {
// Return cached response
return of(new HttpResponse({ body: cachedResponse }));
}
// Make request and cache response
return next.handle(req).pipe(
filter(event => event instanceof HttpResponse),
tap((event: HttpResponse<any>) => {
this.cacheService.setWithExpiry(req.urlWithParams, event.body, 300000); // 5 minutes
}),
share() // Share the observable to prevent duplicate requests
);
}
return next.handle(req);
}
private canCache(req: HttpRequest<any>): boolean {
// Define cacheable URLs
const cacheableUrls = ['/api/lookup', '/api/config', '/api/categories'];
return cacheableUrls.some(url => req.url.includes(url));
}
}
Cache Service Implementation
@Injectable({
providedIn: 'root'
})
export class CacheService {
private cache = new Map<string, { data: any; expiry: number }>();
setWithExpiry(key: string, data: any, ttl: number): void {
const expiry = Date.now() + ttl;
this.cache.set(key, { data, expiry });
}
getWithExpiry(key: string): any | null {
const item = this.cache.get(key);
if (!item) {
return null;
}
if (Date.now() > item.expiry) {
this.cache.delete(key);
return null;
}
return item.data;
}
clear(): void {
this.cache.clear();
}
}
Request/Response Transformation
Transform data in interceptors:
import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } 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(),
version: '1.0'
};
}
return body;
}
private transformResponse(body: any): any {
// Unwrap API response, transform data structure
if (body && body.data) {
return body.data;
}
if (body && body.success !== undefined) {
return body;
}
return body;
}
}
Loading State Interceptor
Track loading states globally:
import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { LoadingService } from './loading.service';
@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
constructor(private loadingService: LoadingService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Skip loading for specific endpoints
if (req.url.includes('/background-sync')) {
return next.handle(req);
}
this.loadingService.setLoading(true);
return next.handle(req).pipe(
finalize(() => {
this.loadingService.setLoading(false);
})
);
}
}
Error Handling in Services
Handle errors in service methods with RxJS operators:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, timeout } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private http: HttpClient) {}
getData(): Observable<any> {
return this.http.get('/api/data').pipe(
timeout(5000), // 5 second timeout
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);
})
);
}
}
Multiple Interceptors
Register multiple interceptors (they execute in order):
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: ErrorInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: LoadingInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: CacheInterceptor,
multi: true
}
]
Execution Order:
- AuthInterceptor (adds token)
- ErrorInterceptor (handles errors)
- LoadingInterceptor (tracks loading)
- CacheInterceptor (caches responses)
Advanced Patterns
Conditional Interceptor Logic
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Skip interceptor for specific URLs
if (req.url.includes('/public')) {
return next.handle(req);
}
// Add token for authenticated routes
const token = this.authService.getToken();
if (token) {
const cloned = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next.handle(cloned);
}
return next.handle(req);
}
}
Request Logging Interceptor
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const startTime = Date.now();
console.log(`[HTTP] ${req.method} ${req.url}`);
return next.handle(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
const duration = Date.now() - startTime;
console.log(`[HTTP] ${req.method} ${req.url} - ${event.status} (${duration}ms)`);
}
})
);
}
}
Best Practices
- Use interceptors for cross-cutting concerns - Authentication, error handling, logging
- Implement proper error handling - User-friendly messages and appropriate actions
- Use caching interceptors - For GET requests to improve performance
- Handle loading states - Show loading indicators during requests
- 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 (401, 403, 404, 500)
- Log errors for debugging - While showing user-friendly messages
- Use environment variables - For API URLs and configuration
- Skip interceptors when needed - For specific endpoints
- Order interceptors correctly - Auth before error handling
Common Patterns
File Upload with Progress
uploadFile(file: File): Observable<HttpEvent<any>> {
const formData = new FormData();
formData.append('file', file);
return this.http.post('/api/upload', formData, {
reportProgress: true,
observe: 'events'
}).pipe(
map(event => {
switch (event.type) {
case HttpEventType.UploadProgress:
const progress = Math.round(100 * event.loaded / event.total!);
return { type: 'progress', progress };
case HttpEventType.Response:
return { type: 'complete', data: event.body };
default:
return event;
}
})
);
}
Request Cancellation
export class SearchService {
private cancelRequest = new Subject<void>();
search(term: string): Observable<any> {
// Cancel previous request
this.cancelRequest.next();
this.cancelRequest = new Subject<void>();
return this.http.get(`/api/search?q=${term}`).pipe(
takeUntil(this.cancelRequest)
);
}
cancel(): void {
this.cancelRequest.next();
}
}
Resources and Further Reading
- π Full Angular HTTP Client Guide - Complete tutorial with advanced examples, troubleshooting, and best practices
- Angular Services Guide - Dependency injection for HTTP services
- Angular Guards Guide - Route protection with HTTP calls
- Angular HTTP Client Documentation - Official Angular docs
- Angular HTTP Interceptors - Official interceptor guide
- RxJS Documentation - Reactive programming operators
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.
Key Takeaways:
- HttpClient - Modern API for HTTP requests with RxJS Observables
- Interceptors - Middleware for cross-cutting concerns
- Authentication - Add tokens automatically to all requests
- Error Handling - Catch and handle errors globally
- Caching - Improve performance with response caching
- Transformation - Transform request/response data
- Loading States - Track loading globally
- Multiple Interceptors - Chain interceptors for complex scenarios
Whether you're building a simple CRUD application or a complex enterprise system, Angular HTTP Client and Interceptors provide the foundation you need. They handle all the HTTP logic while giving you complete control over requests and responses.
What's your experience with Angular HTTP Client and Interceptors? Share your tips and tricks in the comments below! π
π‘ Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.
If you found this guide helpful, consider checking out my other articles on Angular development and frontend development best practices.
Top comments (0)