← Back to Articles         
Angular Functional Interceptors and Route Guards

Angular Functional Interceptors and Route Guards

Published on 02 Oct 2026     9 min read Angular
Older Angular vs Modern Angular

Angular Functional Interceptors and Route Guards

These three questions show the same general modernization in Angular:

BEFORE                           NOW
------                           ---

Class Interceptor                Functional Interceptor
HTTP_INTERCEPTORS                withInterceptors()

Class Route Guard                Functional Route Guard
implements CanActivate           CanActivateFn
constructor DI                   inject()

1. How Are Functional Interceptors Registered Now?

What is a functional interceptor?

A functional interceptor is a function that intercepts Angular HTTP requests and responses.

It uses:

HttpInterceptorFn

instead of creating a class implementing:

HttpInterceptor

Functional interceptors are useful for:

  • JWT tokens
  • Correlation IDs
  • Request logging
  • Global error handling
  • Loading indicators
  • Common headers

Full Simple Example

Suppose Angular calls an ASP.NET Core API:

Angular
   ↓
Auth Interceptor
   ↓
Authorization: Bearer <token>
   ↓
ASP.NET Core Web API

auth.interceptor.ts

import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn =
  (req, next) => {

    const token = 'sample-jwt-token';

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

    return next(authRequest);
  };

The function receives:

(req, next)

where:

  • req = current HTTP request
  • next = passes the request to the next interceptor/HTTP backend

Register in app.config.ts

import { ApplicationConfig } from '@angular/core';

import {
  provideHttpClient,
  withInterceptors
} from '@angular/common/http';

import { authInterceptor }
  from './interceptors/auth.interceptor';

export const appConfig: ApplicationConfig = {

  providers: [

    provideHttpClient(

      withInterceptors([
        authInterceptor
      ])

    )

  ]

};

The important part is:

provideHttpClient(
  withInterceptors([
    authInterceptor
  ])
)

Multiple Functional Interceptors

Suppose we have:

authInterceptor
correlationInterceptor
loggingInterceptor
errorInterceptor

Register them:

provideHttpClient(

  withInterceptors([

    authInterceptor,

    correlationInterceptor,

    loggingInterceptor,

    errorInterceptor

  ])

)

Conceptually:

Component
    ↓
Service
    ↓
HttpClient
    ↓
authInterceptor
    ↓
correlationInterceptor
    ↓
loggingInterceptor
    ↓
errorInterceptor
    ↓
ASP.NET Core API

The order is significant because the interceptors form a chain.


Old vs New Interceptor

Before — Class

@Injectable()
export class AuthInterceptor
  implements HttpInterceptor {

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ) {

    return next.handle(req);
  }
}

Registered using:

{
  provide: HTTP_INTERCEPTORS,
  useClass: AuthInterceptor,
  multi: true
}

Now — Function

export const authInterceptor: HttpInterceptorFn =
  (req, next) => {

    return next(req);

  };

Registered using:

provideHttpClient(
  withInterceptors([
    authInterceptor
  ])
)

So remember:

OLD

HttpInterceptor class
       ↓
HTTP_INTERCEPTORS
       ↓
multi: true


NOW

HttpInterceptorFn
       ↓
withInterceptors()
       ↓
provideHttpClient()

2. How Were Class-Based Route Guards Implemented?

What is a route guard?

A route guard determines whether navigation to or away from an Angular route should be allowed.

For example:

User
 ↓
/admin
 ↓
AuthGuard
 ↓
Is logged in?
 ↓
YES → Admin page

NO  → Login page

Older Angular applications commonly created a guard class.

For CanActivate, the class implemented:

CanActivate

Full Old-Style Class Guard

Suppose only authenticated users can access:

/dashboard

auth.service.ts

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

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  isLoggedIn(): boolean {

    return !!localStorage.getItem('token');

  }

}

auth.guard.ts

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

import {
  ActivatedRouteSnapshot,
  CanActivate,
  Router,
  RouterStateSnapshot
} from '@angular/router';

import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(
    private authService: AuthService,
    private router: Router
  ) {
  }

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean {

    if (this.authService.isLoggedIn()) {

      return true;

    }

    this.router.navigate(['/login']);

    return false;
  }
}

The important part was:

export class AuthGuard implements CanActivate

and the guard logic lived inside:

canActivate()

Register Guard in Route

const routes: Routes = [

  {
    path: 'dashboard',

    component: DashboardComponent,

    canActivate: [
      AuthGuard
    ]
  }

];

Flow:

/dashboard
    ↓
AuthGuard.canActivate()
    ↓
AuthService.isLoggedIn()
    ↓
    ├── true
    │     ↓
    │ Dashboard
    │
    └── false
          ↓
        Login

Why Was @Injectable() Used?

Because the guard was a class using Angular dependency injection:

constructor(
  private authService: AuthService,
  private router: Router
) {}

Therefore Angular needed to create and inject the guard and its dependencies.


Other Class-Based Guards

The same pattern existed for other guard types.

For example:

export class AdminGuard implements CanActivate
export class ChildGuard implements CanActivateChild
export class UnsavedChangesGuard
  implements CanDeactivate<MyComponent>
export class FeatureGuard implements CanMatch

The general pattern was:

@Injectable()
      ↓
Guard class
      ↓
implements Guard Interface
      ↓
constructor injection
      ↓
Guard method

3. How Are Functional Guards Implemented?

Modern Angular supports functional route guards.

Instead of:

class AuthGuard implements CanActivate

we can write:

const authGuard: CanActivateFn

Dependencies can be obtained using:

inject()

Full Functional Guard Example

Using the same AuthService:

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

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  isLoggedIn(): boolean {

    return !!localStorage.getItem('token');

  }

}

auth.guard.ts

import { inject } from '@angular/core';

import {
  CanActivateFn,
  Router
} from '@angular/router';

import { AuthService } from './auth.service';

export const authGuard: CanActivateFn =
  (route, state) => {

    const authService = inject(AuthService);

    const router = inject(Router);

    if (authService.isLoggedIn()) {

      return true;

    }

    return router.createUrlTree(['/login']);
  };

No class.

No:

@Injectable()

No:

implements CanActivate

No constructor.


Route Configuration

import { Routes } from '@angular/router';

import { Dashboard } from './dashboard';

import { authGuard } from './auth.guard';

export const routes: Routes = [

  {
    path: 'dashboard',

    component: Dashboard,

    canActivate: [
      authGuard
    ]
  }

];

The route still uses:

canActivate: [
  authGuard
]

The main difference is that authGuard is now a function, rather than an injectable guard class.


Detailed Flow

Suppose the user enters:

/dashboard

Angular checks:

canActivate: [authGuard]

Then:

User enters /dashboard
          ↓
      Angular Router
          ↓
       authGuard
          ↓
inject(AuthService)
          ↓
   isLoggedIn()
          ↓
       ┌──┴──┐
       │     │
      YES    NO
       │     │
     true    ↓
       │   UrlTree
       │     ↓
       │   /login
       ↓
 Dashboard

Why inject()?

Class guards traditionally used constructor injection:

constructor(
  private authService: AuthService,
  private router: Router
) {}

A functional guard has no constructor.

Therefore Angular provides:

inject()

Example:

const authService = inject(AuthService);

const router = inject(Router);

This obtains dependencies from Angular's dependency-injection context.


Better Redirect Pattern

You may see this:

if (!authService.isLoggedIn()) {

  router.navigate(['/login']);

  return false;
}

It can work, but for guards it is generally cleaner to return a redirect.

For example:

return router.createUrlTree(['/login']);

Or with current Router APIs, a redirect command may be used where appropriate.

The important conceptual distinction is:

true
 ↓
Allow navigation


false
 ↓
Cancel navigation


UrlTree / redirect result
 ↓
Redirect navigation

Functional CanActivate

import { inject } from '@angular/core';
import {
  CanActivateFn,
  Router
} from '@angular/router';

export const authGuard: CanActivateFn =
  () => {

    const router = inject(Router);

    const loggedIn = true;

    return loggedIn
      ? true
      : router.createUrlTree(['/login']);

  };

Functional CanActivateChild

import { CanActivateChildFn } from '@angular/router';

export const childGuard: CanActivateChildFn =
  (childRoute, state) => {

    return true;

  };

Route:

{
  path: 'admin',

  canActivateChild: [
    childGuard
  ],

  children: [
    {
      path: 'users',
      component: Users
    },
    {
      path: 'reports',
      component: Reports
    }
  ]
}

Functional CanDeactivate

Suppose we want to prevent a user from leaving a page containing unsaved changes.

import { CanDeactivateFn } from '@angular/router';

interface CanLeavePage {

  hasUnsavedChanges(): boolean;

}

export const unsavedChangesGuard:
  CanDeactivateFn<CanLeavePage> =

  (component) => {

    if (component.hasUnsavedChanges()) {

      return confirm(
        'You have unsaved changes. Leave this page?'
      );

    }

    return true;
  };

Route:

{
  path: 'edit-product',

  component: EditProduct,

  canDeactivate: [
    unsavedChangesGuard
  ]
}

Functional CanMatch

CanMatch determines whether a route should be considered a match.

Example:

import { inject } from '@angular/core';
import { CanMatchFn } from '@angular/router';

export const adminGuard: CanMatchFn =
  () => {

    const authService = inject(AuthService);

    return authService.isAdmin();

  };

Then:

{
  path: 'dashboard',

  canMatch: [
    adminGuard
  ],

  component: AdminDashboard
},
{
  path: 'dashboard',

  component: UserDashboard
}

This is especially interesting because if:

adminGuard

returns false, Angular can continue checking another matching route.

/dashboard
     ↓
First route
     ↓
adminGuard
     ↓
Is Admin?
   ↓       ↓
 YES      NO
  ↓        ↓
Admin    Try next
route     route
            ↓
      UserDashboard

Class Guard vs Functional Guard

Class-Based Guard Functional Guard
Guard class Function
@Injectable() Not required
implements CanActivate CanActivateFn
Constructor DI inject()
canActivate() method Function body
More boilerplate Less boilerplate

Full Before vs Now Example

Before

@Injectable({
  providedIn: 'root'
})
export class AuthGuard
  implements CanActivate {

  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  canActivate(): boolean {

    if (this.authService.isLoggedIn()) {
      return true;
    }

    this.router.navigate(['/login']);

    return false;
  }
}

Route:

{
  path: 'dashboard',
  component: DashboardComponent,
  canActivate: [AuthGuard]
}

Now

export const authGuard: CanActivateFn =
  () => {

    const authService = inject(AuthService);

    const router = inject(Router);

    return authService.isLoggedIn()
      ? true
      : router.createUrlTree(['/login']);

  };

Route:

{
  path: 'dashboard',
  component: Dashboard,
  canActivate: [authGuard]
}

This is the key transition:

BEFORE

@Injectable()
class AuthGuard
implements CanActivate
constructor(...)
canActivate()


NOW

const authGuard
CanActivateFn
inject(...)
return true / false / redirect

Key Points

Functional Interceptors

Modern interceptors use:

HttpInterceptorFn

and are registered using:

provideHttpClient(
  withInterceptors([
    authInterceptor,
    errorInterceptor
  ])
)

Old Class-Based Guards

They were commonly implemented as:

@Injectable()
export class AuthGuard
  implements CanActivate {

  canActivate() {
    ...
  }
}

Dependencies were obtained using constructor injection.


Functional Guards

Modern guards can be functions:

export const authGuard: CanActivateFn =
  () => {

    const authService = inject(AuthService);

    return authService.isLoggedIn();

  };

Dependencies are obtained using:

inject()

Common functional guard types include:

CanActivateFn
CanActivateChildFn
CanDeactivateFn
CanMatchFn

Interview Questions and Answers

1. How are functional HTTP interceptors registered?

Using withInterceptors() with provideHttpClient():

provideHttpClient(
  withInterceptors([
    authInterceptor,
    errorInterceptor
  ])
)

2. What type represents a functional HTTP interceptor?

HttpInterceptorFn

Example:

export const logInterceptor: HttpInterceptorFn =
  (req, next) => next(req);

3. How were class-based route guards implemented?

A guard class implemented an interface such as:

CanActivate

Example:

@Injectable()
export class AuthGuard
  implements CanActivate {

  canActivate() {
    return true;
  }
}

4. How were dependencies injected into class guards?

Through constructor injection:

constructor(
  private authService: AuthService,
  private router: Router
) {}

5. How are functional guards implemented?

Using function types such as:

CanActivateFn

Example:

export const authGuard: CanActivateFn =
  () => {

    return true;

  };

6. How do functional guards access services?

Using Angular's:

inject()

Example:

const authService = inject(AuthService);

7. What can a CanActivate guard return?

It can return an allowed guard result directly or asynchronously. Common results are:

true
false
redirect result

For example:

return authService.isLoggedIn()
  ? true
  : router.createUrlTree(['/login']);

8. Should a guard redirect using router.navigate()?

For a guard decision, returning a redirect result such as a UrlTree is generally cleaner than imperatively calling navigate() and then returning false.

return router.createUrlTree(['/login']);

9. What is the difference between CanActivate and CanMatch?

CanActivate controls whether an already matched route can be activated.

CanMatch controls whether Angular should consider a route configuration a match. If CanMatch returns false, Angular can continue checking other matching route configurations.


10. Are functional guards more suitable for standalone Angular?

Yes. They integrate naturally with the standalone/provider-based Angular architecture and usually require less boilerplate than guard classes.


Interview Quick Revision

INTERCEPTOR

OLD
HttpInterceptor class
HTTP_INTERCEPTORS
multi: true

NOW
HttpInterceptorFn
withInterceptors()
provideHttpClient()


ROUTE GUARD

OLD
@Injectable()
class AuthGuard
implements CanActivate
constructor injection
canActivate()

NOW
authGuard: CanActivateFn
inject(AuthService)
return true / false / redirect

One-line interview answer

Modern Angular commonly uses functional interceptors registered with provideHttpClient(withInterceptors([...])) and functional route guards such as CanActivateFn, where dependencies are obtained with inject() instead of constructor injection in guard classes.

← Back to Articles