Angular HTTP Interceptor: Sending Bearer Tokens with Every API Request

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.