Angular HTTP Interceptor — Add Auth Token to Every Request Automatically

In this article we going to learn how to use Angular HTTP Interceptor to automatically add an auth token to every HTTP request your app makes.

Whenever  you make an API call which is protected, you need to send a Bearer token in the Authorization header, so you do it manually. Then you make another API call. Manual again. Then another one. Before you know it, every single service in your app has the same header setup repeated over and over. That's messy and it's a nightmare to update if the token logic ever changes.

HTTP Interceptors solve this in a clean way. You set it up once, and Angular automatically adds the token to every outgoing request. You never touch headers in your services again.

This tutorial shows how to:

  • Understand what an HTTP Interceptor is and how it works
  • Create a functional HTTP Interceptor in Angular 15+
  • Read the auth token from localStorage and attach it to requests
  • Skip the token for specific requests like login and register
  • Handle 401 unauthorized responses and redirect to login
  • Register the interceptor in the app

What is an HTTP Interceptor

Think of an interceptor like a middleware for your HTTP calls. Every request your app makes passes through it before going to the server. Every response comes back through it before reaching your code.

You can use that to :

  • Add headers to every request automatically
  • Log all API calls for debugging
  • Show a loading spinner when any request is in progress
  • Catch 401 or 403 errors globally and redirect to login

You write the logic once, it applies everywhere. That's the whole point.


Step 1 : Create the Auth Interceptor

In Angular 15+, interceptors can be written as plain functions. Much cleaner than the old class-based way. No need to implement an interface or inject via a class.

Create a new file auth.interceptor.ts :

import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router);

  // Read token from localStorage
  const token = localStorage.getItem('token');

  // Clone the request and add the Authorization header
  const authReq = token
    ? req.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      })
    : req;

  return next(authReq).pipe(
    catchError(error => {
      if (error.status === 401) {
        localStorage.removeItem('token');
        router.navigate(['/login']);
      }
      return throwError(() => error);
    })
  );
};

A few things happening here. First it reads the token from localStorage. If a token exists, it clones the request and adds the Authorization header. Cloning is important — HTTP requests in Angular are immutable, you can't modify them directly. So you clone, modify the clone, and pass that along.

Then it handles the response with catchError. If the server returns 401, it means the token is expired or invalid. We clear it from localStorage and redirect to login automatically.

If there's no token, the original request goes through unchanged. That handles open endpoints like login and register.


Step 2 : Register the Interceptor

How you register the interceptor depends on whether you're using AppModule or the new standalone bootstrap.

For standalone apps (Angular 17+) — in main.ts :

import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { authInterceptor } from './app/auth.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(
      withInterceptors([authInterceptor])
    )
  ]
}).catch(err => console.error(err));

withInterceptors() takes an array so you can chain multiple interceptors if needed. They run in order.

For NgModule apps — in app.module.ts :

import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';

@NgModule({
  imports: [HttpClientModule],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ]
})
export class AppModule {}

Note — for NgModule apps, the interceptor needs to be a class that implements HttpInterceptor. The functional style only works with the new standalone provideHttpClient approach. If you're on NgModule, see the class-based version in Step 5 below.


Step 3 : Skip the Token for Specific Requests

Right now every request gets the token. But login, register, forgot password — these are public endpoints. They don't need a token. Actually sending a token to login doesn't cause problems usually, but it's cleaner to skip it.

One way is to check the URL and skip adding the header for known public routes :

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router);

  // List of URLs that don't need auth token
  const publicUrls = ['/api/auth/login', '/api/auth/register', '/api/auth/forgot-password'];

  const isPublicUrl = publicUrls.some(url => req.url.includes(url));

  const token = localStorage.getItem('token');

  const authReq = (!isPublicUrl && token)
    ? req.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      })
    : req;

  return next(authReq).pipe(
    catchError(error => {
      if (error.status === 401 && !isPublicUrl) {
        localStorage.removeItem('token');
        router.navigate(['/login']);
      }
      return throwError(() => error);
    })
  );
};

The publicUrls array holds the endpoints that don't need a token. For any request whose URL matches one of those, the token is skipped. Also the 401 redirect is skipped for public URLs — otherwise a failed login attempt would redirect you to login, which is already where you are.


Step 4 : Add a Loading Interceptor on Top

Since we're talking interceptors, here's a bonus one that shows a loading indicator while any request is in progress. Very useful for any app.

Create loading.interceptor.ts :

import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { finalize } from 'rxjs';
import { LoadingService } from './loading.service';

export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
  const loadingService = inject(LoadingService);

  loadingService.show();

  return next(req).pipe(
    finalize(() => loadingService.hide())
  );
};

LoadingService just holds a boolean Signal or BehaviorSubject that your spinner component listens to :

import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class LoadingService {
  isLoading = signal(false);

  show() { this.isLoading.set(true); }
  hide() { this.isLoading.set(false); }
}

Register both interceptors in main.ts :

provideHttpClient(
  withInterceptors([authInterceptor, loadingInterceptor])
)

They run in the order listed. Auth token gets added first, then the loading state is managed. finalize() runs when the request completes whether it succeeds or fails, so the spinner always disappears.


Step 5 : Class-Based Interceptor for NgModule Apps

If you're still on NgModule, here's the class-based version of the auth interceptor :

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Router } from '@angular/router';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private router: Router) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = localStorage.getItem('token');

    const authReq = token
      ? req.clone({
          setHeaders: {
            Authorization: `Bearer ${token}`
          }
        })
      : req;

    return next.handle(authReq).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          localStorage.removeItem('token');
          this.router.navigate(['/login']);
        }
        return throwError(() => error);
      })
    );
  }
}

Same logic, just class-based. The intercept() method is where everything happens. req.clone() works the same way. catchError handles the 401 the same way.

Register it in AppModule with HTTP_INTERCEPTORS as shown in Step 2.


Testing the Interceptor

To verify it's working, open your browser DevTools, go to the Network tab, and make any API call from your app. Click on the request and check the Request Headers section.

You should see :

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

If the header is there, the interceptor is working. If not, check that the token is actually in localStorage and that provideHttpClient(withInterceptors(...)) is configured correctly in main.ts.

Also test the 401 flow — temporarily change your token to something invalid and make a request. Your app should clear the token and redirect to /login automatically.


Summary

You learned how to create and use Angular HTTP Interceptors to handle auth tokens automatically. You covered :

  • What an interceptor is and why it's better than adding headers manually in every service
  • Creating a functional HttpInterceptorFn in Angular 15+ that adds a Bearer token to every request
  • Registering the interceptor using provideHttpClient(withInterceptors()) in standalone apps
  • Skipping the token for public endpoints like login and register
  • Handling 401 errors globally and redirecting to login automatically
  • Building a loading interceptor using Signals for a global spinner
  • The class-based interceptor approach for NgModule apps

Once the interceptor is set up, you never think about headers again. Every service just makes the HTTP call and the token is there automatically. That's exactly how it should work.

I hope you like this article...

Happy coding! 🚀

Post a Comment

0 Comments