When building an Angular application that communicates with secured APIs, we usually need to send an authentication token with every protected HTTP request.
Instead of manually adding the Authorization header in every service, Angular provides HTTP Interceptors.
An HTTP interceptor allows us to intercept outgoing HTTP requests, modify them, and then pass them to the server.
What is an HTTP Interceptor?
An HTTP Interceptor is a mechanism in Angular that sits between our application and the backend API.
The flow looks like this:
Angular Component → Service → HTTP Interceptor → API
For example, without an interceptor, we might have to write:
this.http.get('/api/users', {
headers: {
Authorization: `Bearer ${token}`
}
});
This becomes repetitive when we have many API calls.
With an interceptor, we can write:
this.http.get('/api/users');
The interceptor automatically adds:
Authorization: Bearer <access-token>
to the request.
Creating a Functional Interceptor
In modern Angular applications, especially standalone applications, functional interceptors are a clean and recommended approach.
We can generate an interceptor using:
ng generate interceptor auth
Then implement it like this:
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('access_token');
if (token) {
const authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next(authReq);
}
return next(req);
};
How does this work?
The interceptor receives two important parameters:
(req, next)
req represents the outgoing HTTP request.
next is responsible for passing the request to the next interceptor or ultimately to the backend.
Because Angular HTTP requests are immutable, we cannot directly modify the request.
Therefore, we use:
req.clone()
to create a modified copy.
Then we add the Authorization header:
setHeaders: {
Authorization: `Bearer ${token}`
}
The final request becomes:
GET /api/users
Authorization: Bearer eyJhbGciOi...
Registering the Interceptor
For a standalone Angular application, we can register the interceptor in app.config.ts.
import { ApplicationConfig } from '@angular/core';
import {
provideHttpClient,
withInterceptors
} from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor])
)
]
};
After registering it, all requests made through Angular’s HttpClient will pass through the interceptor.
For example:
this.http.get('/api/users');
The interceptor automatically converts it into a request containing:
Authorization: Bearer <token>
Using an AuthService
In a real-world application, I prefer not to access localStorage directly from the interceptor.
Instead, authentication-related operations can be handled by an AuthService.
For example:
@Injectable({
providedIn: 'root'
})
export class AuthService {
getToken(): string | null {
return localStorage.getItem('access_token');
}
setToken(token: string): void {
localStorage.setItem('access_token', token);
}
clearToken(): void {
localStorage.removeItem('access_token');
}
}
The interceptor can then use the service:
import { inject } from '@angular/core';
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getToken();
if (!token) {
return next(req);
}
const authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next(authReq);
};
This provides better separation of responsibilities.
AuthService → manages authentication/token logic.
Interceptor → attaches the token to HTTP requests.
Service → communicates with the API.
Should We Send the Token to Every Request?
Not necessarily.
This is an important point in production applications.
We should avoid sending authentication tokens to:
- Login APIs
- Public APIs
- Token refresh endpoints
- Third-party domains
For example:
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getToken();
if (
!token ||
req.url.includes('/login') ||
req.url.includes('/refresh-token')
) {
return next(req);
}
return next(
req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
})
);
};
An even better production approach is to check whether the request belongs to your application’s API domain or API path before attaching the token.
Handling 401 Unauthorized
Adding the token is only one part of authentication.
What happens when the token expires?
The backend may return:
401 Unauthorized
We can handle this centrally using the interceptor.
Conceptually, the flow becomes:
API Request
↓
Interceptor
↓
Attach Access Token
↓
Backend
↓
401 Unauthorized
↓
Refresh Access Token
↓
Retry Original Request
A simplified example:
return next(authReq).pipe(
catchError((error) => {
if (error.status === 401) {
// Refresh token or redirect to login
}
return throwError(() => error);
})
);
In a production application, token refresh needs additional handling to prevent multiple API requests from triggering multiple refresh calls simultaneously.
Why Use an HTTP Interceptor?
There are several benefits.
1. Centralized Authentication
We don’t need to add the Authorization header manually to every API call.
2. Less Duplicate Code
Instead of:
this.http.get(url, { headers });
this.http.post(url, data, { headers });
this.http.put(url, data, { headers });
we can simply write:
this.http.get(url);
this.http.post(url, data);
this.http.put(url, data);
3. Easier Maintenance
If the authentication mechanism changes, we can update the interceptor rather than modifying every service.
4. Centralized Error Handling
We can also handle common HTTP errors such as:
401 → Authentication issue
403 → Forbidden
500 → Server error
from a centralized location.
Interview Explanation
If an interviewer asks:
“How do you send a Bearer token with every Angular HTTP request?”
A good answer would be:
“I use an Angular HTTP interceptor to centrally handle authentication headers. The interceptor retrieves the access token from an authentication service, clones the immutable HTTP request, adds the
Authorization: Bearer <token>header, and passes the cloned request to the next handler. I also make sure public endpoints and third-party requests don’t receive the token. For expired tokens, I can handle 401 responses centrally by refreshing the token and retrying the failed request.”
If they ask “Why do you use clone()?”, answer:
“Angular’s HttpRequest is immutable, so we cannot directly modify the existing request. We create a cloned request with the required headers and pass that cloned request forward.”
If they ask “What happens when the token expires?”, answer:
“The API returns 401. The interceptor can catch that response, call the refresh-token API, update the access token, and retry the original request. If the refresh fails, I clear the authentication state and redirect the user to login.”
Final Takeaway
Angular HTTP Interceptors are useful for implementing cross-cutting HTTP functionality such as:
- Adding Bearer tokens
- Adding common headers
- Logging requests
- Handling errors
- Refreshing expired tokens
- Showing/hiding loading indicators
- Adding correlation or request IDs
For authentication specifically, the key pattern is:
Component
↓
Service
↓
HttpClient
↓
Auth Interceptor
↓
Add Bearer Token
↓
Backend API
This keeps authentication logic centralized, reduces duplicate code, and makes Angular applications easier to maintain.