Making API calls is one of the most common tasks in Angular applications. Whether you are building a simple CRUD interface or a large-scale enterprise system, your app will frequently communicate with backend services using HTTP requests. While Angular provides the HttpClient service for making calls, repeating logic such as attaching authentication tokens, handling errors, logging requests, or modifying headers across multiple services becomes difficult to maintain.
This is where Angular Interceptors become extremely powerful.
Angular Interceptors allow you to intercept outgoing HTTP requests and incoming responses. This means you can centralize common logic and keep your application clean and maintainable.
This article provides a complete, step-by-step explanation of Angular Interceptors, how they work, common use cases, and implementation examples with best practices.
1. What Are Angular Interceptors
Angular Interceptors are classes that implement the HttpInterceptor interface. They act like middleware that sits between your app and the backend API.
When you make an HTTP request:
The request first passes through all configured interceptors.
The backend API processes it.
The response then passes back through the interceptors in reverse order.
This flow allows you to:
Automatically add authentication headers
Log request or response details
Catch and manage errors globally
Modify request URLs dynamically
Handle loading indicators
Retry failed requests
In short, interceptors help in centralizing reusable logic for all API calls.
2. Creating Your First Angular Interceptor
Let’s start by creating a basic interceptor using the Angular CLI.
ng generate interceptor interceptors/auth
This creates:
src/app/interceptors/auth.interceptor.ts
Inside the generated file, you get a default structure.
2.1 Basic Interceptor Template
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req);
}
}
This is the simplest form, where the request passes unchanged to the next handler.
3. Adding Authorization Headers Automatically
One of the most common uses of interceptors is attaching authentication tokens (JWT tokens) to all outgoing requests.
3.1 Example: Adding Authorization Header
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem('token');
const modifiedRequest = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(modifiedRequest);
}
}
Now, every HTTP request automatically gets the Authorization header attached.
4. Registering the Interceptor in App Module
Angular does not enable interceptors automatically. You must register them in the providers array.
4.1 Add in app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './interceptors/auth.interceptor';
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
})
export class AppModule {}
The multi: true property ensures Angular can use multiple interceptors.
5. Logging HTTP Requests and Responses
Interceptors can also be used to log data for debugging or analytics.
5.1 Logging Interceptor Example
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log('Outgoing request:', req.url, req.method);
return next.handle(req).pipe(
tap(event => {
if (event.type === 4) {
console.log('Response received:', event);
}
})
);
}
}
This logs every request and response passing through the interceptor.
6. Global Error Handling with Interceptors
Handling errors in every service file is repetitive. Instead, you can catch errors globally in one place.
6.1 Error Handling Example
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError(err => {
if (err.status === 401) {
console.log('Unauthorized access');
}
if (err.status === 500) {
console.log('Server error occurred');
}
return throwError(() => err);
})
);
}
}
This approach keeps your services clean.
7. Adding a Loader / Spinner Using Interceptors
Many applications require showing a loading spinner while API calls are in progress. This can be handled in one place using an interceptor.
7.1 Loader Example
@Injectable()
export class LoaderInterceptor implements HttpInterceptor {
private requestCount = 0;
constructor(private loaderService: LoaderService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.requestCount++;
this.loaderService.show();
return next.handle(req).pipe(
finalize(() => {
this.requestCount--;
if (this.requestCount === 0) {
this.loaderService.hide();
}
})
);
}
}
This ensures the loader indicator appears and disappears correctly.
8. Retrying Failed HTTP Requests
Interceptors can automatically retry failed calls due to network issues.
8.1 Retry Example
import { retry } from 'rxjs/operators';
@Injectable()
export class RetryInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
retry(2)
);
}
}
This retries the request twice before failing.
9. Modifying Request URLs
You may need to prefix all API URLs with a base URL.
9.1 Example: Adding Base URL
@Injectable()
export class ApiPrefixInterceptor implements HttpInterceptor {
private baseUrl = 'https://api.example.com/';
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const updatedRequest = req.clone({
url: this.baseUrl + req.url
});
return next.handle(updatedRequest);
}
}
This allows you to use short URLs in your services.
10. Order of Interceptors
Angular executes interceptors in the order they are provided:
Outgoing Request Order
Interceptor1
Interceptor2
Interceptor3
Incoming Response Order
Interceptor3
Interceptor2
Interceptor1
This means registration order influences behavior.
11. Full Real-World Example Setup
In a real application, you might use four interceptors:
ApiPrefixInterceptor
AuthInterceptor
LoggingInterceptor
ErrorInterceptor
11.1 Register All Interceptors
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: ApiPrefixInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }
]
This setup centralizes all API-related logic.
12. Best Practices When Using Angular Interceptors
Keep interceptors focused on one responsibility.
Do not perform heavy logic inside interceptors.
Avoid multiple clone operations unless necessary.
Use interceptors for cross-cutting concerns only.
Always use multi: true when registering.
Combine logging and error handling carefully to avoid duplication.
Do not modify responses unnecessarily.
13. When Should You Use Interceptors
Use interceptors when you need to:
Attach tokens to requests
Log requests or responses
Catch global errors
Add base URLs
Show and hide loaders
Implement retry strategies
Transform requests or responses
They help simplify your codebase significantly.
14. When Not To Use Interceptors
Avoid using interceptors when:
You need feature-specific logic (use services instead)
You require user interaction inside the interceptor
You want to handle business logic (interceptors are not for business rules)
The modification applies only to one endpoint
Use interceptors only for global logic.
Conclusion
Angular Interceptors are one of the most powerful features available to simplify HTTP communication across your application. By using interceptors, you can centralize repeated tasks such as adding headers, handling errors, logging, and managing loaders. This helps maintain a clean architecture and reduces redundancy in your services.
You now understand: