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

  1. Interceptors implement the HttpInterceptor interface
  2. Must implement intercept() method
  3. Work with HttpRequest and HttpHandler
  4. Must be provided in application config
  5. 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 tap operators 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 catchError to 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 finalize operator 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 retry operator
  • 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 setHeaders to 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 provideHttpClient with withInterceptors
  • 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 HttpInterceptorFn type

  • Simpler, tree-shakable

  • use inject() for dependencies

  • Recommended for new projects

  • Class-based Interceptors (Legacy)

  • Implement HttpInterceptor interface

  • Use 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?

AAuthentication: 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 implement
  • HTTP_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

  1. ✅ Keep interceptors focused : One intercepto per consern
  2. ✅ Oder matter: Place interceptors in logical order
  3. ✅ Immutablility: Always use clone() to modify requests
  4. ✅ Error Handling: Don't swallow errors unless intentional
  5. ✅ Performance: Be mindful of interceptor overhead
  6. ✅ Testing: Write unit tests for interceptors
  7. ✅ Documentation: Comment complex logic
  8. ✅ Use HttpContext: for request-specific behaviour
  9. ✅ Avoid side effect: Keep interceptors pure when possible
  10. ✅ 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

Popular posts from this blog