Angular Interceptors
Angular Interceptors
What are HTTP Interceptors?
HTTP Interceptors are powerful feature in Angular that allow you to intercept and modify HTTP requests and response globally. They sit between your application and backend server, enabling to:
- Add authentication tokens to requests
- Log HTTP traffic
- Handle errors globally
- Show/hide loading spinners
- Modify request/response headers
- Cache responses
- Retry failed request
Key Concepts for Interview
- Interceptors implement the
HttpInterceptorinterface - Must implement
intercept()method - Work with
HttpRequestandHttpHandler - Must be provided in application config
- Executed in the order they are provided
Implementation Examples
1. Authentication Interceptor
Puspose: Automatically add authentication token to all outoging HTTP requests.
import {HttpInterceptorFn} from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
// Get token form localStorage or Service
const token = localStorage.getItem('authToken');
if(token){
// clone the request and add authorization header
const cloneRequest = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next(cloneRequest);
}
return next(req);
};
Key Points:
- HttpRequest is immutale, so we use
clone()to modify it - Adds Bearer token to Authorization header
- Only adds token if it exists
2. Logging Interceptor
Purpose: Log all HTTP request and response with timing information.
import {HttpInterceptorFn} from '@angular/common/http';
import {tap} from 'rxjs/operators';
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const startTime = Date.now();
console.log(`Request: ${req.method} ${req.url}`);
return next(req).pipe(
tap({
next: (event) => {
const elapsed = Date.now()- startTime();
console.log(`✅ Response received in ${elapsed}ms`, event);
},
error: (error) => {
const elapsed = Date.now()- startTime();
console.log(`❌ Request failed in ${elapsed}ms`, error);
}
});
);
};
Key Points:
- Uses RxJS
tapoperators to log without modiyfing the stream - Tracks request timing
- Logs both successful and failed requests
3. Error Handling Interceptor
Purpose: Centralized error handling for all HTTP requests.
import {HttpInterceptorFn, HttpErrorResponse} from '@angular/common/http';
import {catchError, throwError} from 'rxjs';
import {inject} from '@angular/core';
import {Router} from '@angular/router';
export const errorHandlerInterceptor: HttpInterceptorFn = (req, next) => {
const router = Inject(Router);
return next(req).pipe(
catchError((error: HttpErrorResponse)=> {
let errorMessage = '';
if(error.error instanceof ErrorEvent){
// Client-side error
errorMessage = `Client Error: ${error.error.message}`;
} else {
// Server side error
errorMessage = `Server Error code: ${error.status}\nMessage: ${error.message}`;
// Handle specific status code
switch(error.status){
case 401:
// Unauthorized - redirect to login
router.nevigate(['/login']);
break;
case 403:
// Forbidden
alert('Access Denied!');
break;
case 404:
// Not Found
alert('Resource not found!');
break;
case 500:
// Internal Server Error
alert('Server error. Please try again later.');
break;
}
}
console.error(errorMessage);
return throwError(()=> error);
})
);
};
Key Points:
- Use
catchErrorto intercept errors - Differentiates between client-side and server-side errors
- Handles specific HTTP status code
- Can redirect user (e.g. to login on 401)
- Re-throws error for component-level handling
4. Loading Spinner Interceptor
Purpose: Show/hide loading spinner during Http requests.
import {HttpInterceptorFn} from '@angular/common/http';
import {Inject} from '@angular/core';
import {finalize} from 'rxjs/operator';
import {LoadingService} from '../services/loading.service';
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
const loadingService = inject(LoadingService);
// Show loading Spinner
loadingService.show();
return next(req).pipe(
finalize(() => {
// Hide loading spinner when request complets
loadingService.hide();
})
);
};
Key Points:
- Use
finalizeoperator to execute code after completion (success of error) - Requires a LoadingService to manage loading state
- Works with multiple concurrent requests
5. Loading Service (Supporting Service)
import { Injectable } from'@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
provideIn: 'root'
})
export class LoadingService {
private loadingSubject = new BehaviourSubject<boolean>(false);
public loading$ = this.loadingSubject.asObservable();
private activeRequests = 0;
show():void {
this.activeRequests++;
this.loadingSubject.next(true);
}
hide(): void{
this.activeRequests--;
if(this.activeRequest<=0){
this.activeRequest =0;
this.loadingSubject.next(flase);
}
}
}
Key Point:
- track multiple concurrent requests with counter
- Only hides spinner when all request complate
- Expose observable for components to subscribe to
6. Caching Interceptor
Purpose: Cache GET requests to reduce server load and improve performance.
import { HttpInterceptorFn , HttpResponse} from '@angular/commom/http';
import { inject } from '@angular/core';
import { of } from 'rxjs';
import { tap } from 'rxjs/operator';
import { CacheService } from '../services/cache.service';
export const cachingInterceptor: HttpInterceptorFn = (req, next) => {
const cacheService = inject (CacheService);
// Only cache GET request
if(req.method !=='GET') return next(req);
// Check if response is in cache
const cachedResponse = cacheService.get(req.url);
if(cachedResponse){
console.log('Returning cached response');
return of(cachedResponse);
}
// if not cached, proceed with request and cache the response
return next(req).pipe(
tap(event => {
if(event instanceOf HttpResponse){
cacheService.set(req.url, event);
}
})
);
};
Key Points:
- Only caches GET requests
- Return cached response immediatly using
of() - Caches new response for future use
7. Cache Service
import { Injectable } from '@angular/core';
import {HttpResponse } from '@angular/common/http';
@Injectable({
proivdeIn: 'root'
})
export class CacheService {
private cache = new Map<string, HttpResponse<any>>();
private maxAge = 300000; 5 minutes in milliseconds
set(url: string, response: HttpResponse<any>): void {
this.cache.set(url, response);
// Auto-clear cache after maxAge
setTimeout(() => {
this.cache.delete(url);
}, this.maxAge);
}
get(url: string): HttpResponse<any> | undefined {
return this.cache.get(url);
}
clear(): void{
this.cache.clear();
}
}
Key Points:
- Use Map for efficient key-value stroage
- Implements automatic cache expiration
- Provides method to manually clear cache
8. Retry Interceptor
Purpose: Automatically retry failed requests.
import {HttpInterceptorFn, HttpErrorResponce } from '@angular/common/http';
import {retry, timer} from 'rxjs';
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
retry({
count: 3,
delay: (error: HttpErrorResponse, retryCount)=>{
//Only retry on specific error codes
if(error.status===500 || error.status===503){
console.log(`retry attempt ${retryCount} for ${req.url}`);
// Exponential backoff: 1s, 2s, 4s
return timer(Math.pow(2, retryCount-1)*1000);
}
throw error;
}
})
);
};
Key Points:
- Use RxJS
retryoperator - Implements exponential backoff strategy
- Only retries on specific error code (500, 503)
- Configurable retry count
9. Header Modification Interceptor
Purpose: Add custom headers to all requests.
import {HttpInterceptorFn} form '@angualr/common/http';
export const headerInterceptor: HttpInterceptorFn = (req, nex) => {
const modifiedReq = req.clone({
setHeaders: {
'Content-Type' : 'application/Json',
'X-App-Version': '1.0.0',
'x-Request-ID' : crypto.randomUUID()
}
});
return next(modifiedReq);
};
Key Point:
- Add multiple headers in one operation
- Can set custom headers for tracking/debugging
- Uses
setHeadersto add or override headers
10. Register Interceptors in App Config
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';
import { errorHandlerInterceptor } from './interceptors/error-handler.interceptor';
import { loadingInterceptor } from './interceptors/loading.interceptor';
import { cachingInterceptor } from './interceptors/caching.interceptor';
import { retryInterceptor } from './interceptors/retry.interceptor';
import { headerInterceptor } from './interceptors/header.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(
withInterceptors([
loggingInterceptor, // First: log the request
headerInterceptor, // secont: Add customHeaders
authInterceptor, // Third: Add auth token
cachingInterceptor, // Fourth: Check cache
retryInterceptor, // Fifth: Retry logic
loadingInterceptor, // sixth: Show loading
errorHandlerInterceptor, // Last: handle error
])
)
]
};
Key Points:
- Order matters! Interceptors execute in the order provided
- Use
provideHttpClientwithwithInterceptors - Response processing happens in revese order
Common Interview Questions & Answers
Q1: What is the execution order of interceptors?
A: Interceptors execute in the order they are provided in the withInterceptor() array for Outgoing request, and in reverse order for incoming response.
Example:
Request flow: A -> B -> C -> Server
Response Flow: Server -> C -> B -> A
Q2: Can you modify the request in an interceptors?
A: Yes, but HttpRequest is immutable. We must use the clone() method to create a modified copy.
Example:
const modifiedReq = req.clone({
setHeaders: { 'Autherization': `Bearer token`}
});
Q3: How do you handle errors in interceptors?
Answer: Use RxJS catchError operators in the pipe() method after calling next(req).
Example:
return next(req).pipe(
catchError(error: HttpErrorResponse) => {
console.log('Error occourd:', error);
return throwError(()=> error);
}
)
Q4: what's the difference between functional and class-based interceptors?
A:
Functional Interceptors (Angular 15+):
Use
HttpInterceptorFntypeSimpler, tree-shakable
use
inject()for dependenciesRecommended for new projects
Class-based Interceptors (Legacy)
Implement
HttpInterceptorinterfaceUse constructor for Dependency injection
More verbose Functional Example:
export class myInterceptor: HttpInterceptorFn = (req, next) => {
const service = inject(MyService);
requrn next(req);
};
Class-based Example:
@Injectable()
expoer class MyInterceptor implements HttpInterceptor {
constructor(private service : MyService){}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>{
return next.handle(req);
}
}
Q5: Can you skip an interceptor for specific requests?
A: Yes, use HttpContext to pass metadata with requests and check it in the intrceptors.
Example:
// Define context token
import { HttpContext, HttpContextToken } from '@angular/common/http';
export const SKIP_Auth = new HttpContextToken<boolean>(() => false);
// In Interceptor
export const authInterceptor: HttpInterceptosFn = (req, next) => {
if(req.context.get(SKIP_Auth)){
return next(req);
}
// ... add auth logic
const token = localStroage.getItem('authToken');
const cloneReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}`}
});
return next(cloneReq);
};
// Uage in service
this.http.get('/api/public', {
context: new HttpContext().set(SKIP_Auth, true)
});
Q6: How do you handle multiple concurrent requests in loading interceptos?
A Use a counter to track active requests and only loading when counter reaches to zero.
private activeRequests = 0;
show (): void{
this.activeRequests++;
this.loadingSubject.next(true);
}
hide (): void {
this.activeRequests--;
if(this.activeRequests < =0){
this.activeRequests =0;
this.loadingSubject.next(false);
}
}
Q7: Can you have multiple interceptors for the same purpose?
A: Yes, but it's generally batter to have one interceptos per consern. However, we can have multiple if they serve different purpose (e.g. one for logging requests, another for logging response).
Q8: How do you test Interceptors?
Q9: What is the real-world use cases for Interceptors?
A: Authentication: Add Jwt tokens to request Logging: Moniter API call for dabugging Error Handling: Global error handling and user notigications Loading state: show/hide spinners Caching: Cache GET requets for performance Request Transformation: Convet data formates Retry logic: Retry failed request automatically API Versioning: Add version headers to all requests Request Throttling: Limit request here offline support: Queue requests when offline
Q10: What's the difference between HttpInterceptor and HTTP_INTERCEPTORS?
A
HttpInterceptor: Interface that class-based interceptors implementHTTP_INTERCEPTORS: Injection token used to provide interceptors in lagacy Angular apps- Modern Angular (15+) use functional interceptors with
withInterceptor()instead
Advance Topics
Request Transformation Example
export const jsonToFormDataInterceptor: HttpInterceptorFn = (req, next) => {
if( req.body && req.headers.get('Content-Type') === 'multipart/form-data'){
const formData = new FormData();
Object.key(key.body).forEach( key => {
formData.append(key, req.body[key]);
});
const modifiedReq = req.clone({
body: formData,
headers: req.headers.delete('Content-Type') // Let browser set it
});
return next(modifiedReq);
}
return next(req);
}
Token Referesh Interceptor
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject} from '@angualr/core';
import { catchError, switchMap, throwError } from 'rxjs';
import { AuthService } form '..services/auth.service';
export const tokenRefteshInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if(error.status ===401 && !req.url.includes('/refresh')){
// token Expired, try to refresh
return authService.refreshToken().pipe(
switchMap ( newToken =>{
// Retry original request with new tokens
const clonedReq = req.clone({
setHeaders : { Autherization: `Bearer ${newToken}` }
});
return next(clonedReq);
}),
catchError( refereshError => {
// Refresh Failed, redirect to login
authService.logout();
return throwError(() => refreshError);
})
);
}
return throwError(()=> error);
})
);
};
Best Pratices
- ✅ Keep interceptors focused : One intercepto per consern
- ✅ Oder matter: Place interceptors in logical order
- ✅ Immutablility: Always use
clone()to modify requests - ✅ Error Handling: Don't swallow errors unless intentional
- ✅ Performance: Be mindful of interceptor overhead
- ✅ Testing: Write unit tests for interceptors
- ✅ Documentation: Comment complex logic
- ✅ Use HttpContext: for request-specific behaviour
- ✅ Avoid side effect: Keep interceptors pure when possible
- ✅ Handle edge cases: Consider timeout, retries, cancellation
Summary
Interceptors are powerful middleware for Angular HTTP request. key takeways:
- Use fontional interceptors (
HttpInterceptorFn) in modern Angular - Register with
provideHttpClient(withInterceptors([...])) - Order matters with request/response processing
- Always clone request before modifying
- Use RxJS operators for async operations
- Test thorougly with
HttpClientTestingModule - Keep interceptors focused and maintainable
Comments
Post a Comment