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 requestnext= 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 asCanActivateFn, where dependencies are obtained withinject()instead of constructor injection in guard classes.