This guide explains how to create and use functional route guards and HTTP interceptors in the Angular 18 Archetype.
Overview
Angular 18 uses functional APIs for guards and interceptors instead of class-based implementations:
- Guards: Use
CanActivateFn for route protection
- Interceptors: Use
HttpInterceptorFn for HTTP request/response handling
This archetype uses the modern functional approach introduced in Angular 15+. Class-based guards and interceptors are deprecated.
Route guards
Creating an auth guard
The authGuard at src/app/core/providers/auth.guard.ts demonstrates the recommended pattern:
src/app/core/providers/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { environment } from '@env/environment';
import { AuthStore } from '@services/state/auth.store';
/**
* Guard function to check if the user is authenticated.
* @returns True if authenticated, otherwise redirects to login page
*/
export const authGuard: CanActivateFn = () => {
if (environment.securityOpen) return true;
const authStore = inject(AuthStore);
if (authStore.isAuthenticated()) return true;
const router = inject(Router);
return router.createUrlTree(['/auth', 'login']);
};
Using the guard
Apply guards to routes in your routing configuration:
import { Routes } from '@angular/router';
import { authGuard } from './core/providers/auth.guard';
export const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./features/dashboard/dashboard.component'),
canActivate: [authGuard]
},
{
path: 'profile',
loadComponent: () => import('./features/profile/profile.component'),
canActivate: [authGuard]
},
{
path: 'auth/login',
loadComponent: () => import('./features/auth/login.component')
}
];
Creating a role-based guard
Define the guard function
Create a guard that checks user roles:src/app/core/providers/role.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthStore } from '@state/auth.store';
export const roleGuard = (allowedRoles: string[]): CanActivateFn => {
return () => {
const authStore = inject(AuthStore);
const router = inject(Router);
const user = authStore.user();
const hasRole = allowedRoles.includes(user.role);
if (hasRole) return true;
return router.createUrlTree(['/unauthorized']);
};
};
Use the parameterized guard
import { roleGuard } from './core/providers/role.guard';
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./features/admin/admin.component'),
canActivate: [roleGuard(['admin'])]
},
{
path: 'dashboard',
loadComponent: () => import('./features/dashboard/dashboard.component'),
canActivate: [roleGuard(['user', 'admin'])]
}
];
Guard types
Angular provides several guard types:
CanActivateFn
Protects route activation:
export const canActivateGuard: CanActivateFn = (route, state) => {
const authStore = inject(AuthStore);
return authStore.isAuthenticated();
};
CanActivateChildFn
Protects child routes:
import { CanActivateChildFn } from '@angular/router';
export const canActivateChildGuard: CanActivateChildFn = (route, state) => {
const authStore = inject(AuthStore);
return authStore.isAuthenticated();
};
CanDeactivateFn
Prevents navigation away from a route:
import { CanDeactivateFn } from '@angular/router';
export interface CanComponentDeactivate {
canDeactivate: () => boolean;
}
export const canDeactivateGuard: CanDeactivateFn<CanComponentDeactivate> = (component) => {
return component.canDeactivate ? component.canDeactivate() : true;
};
Usage:
export class FormComponent implements CanComponentDeactivate {
hasUnsavedChanges = false;
canDeactivate(): boolean {
if (this.hasUnsavedChanges) {
return confirm('You have unsaved changes. Do you want to leave?');
}
return true;
}
}
HTTP interceptors
Creating an auth interceptor
The authInterceptor at src/app/core/providers/auth.interceptor.ts shows how to add authentication and handle errors:
src/app/core/providers/auth.interceptor.ts
import { HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { NULL_USER_ACCESS_TOKEN } from '@domain/userAccessToken.type';
import { AuthStore } from '@services/state/auth.store';
import { NotificationsStore } from '@services/state/notifications.store';
import { catchError, throwError } from 'rxjs';
const AUTH_ERROR_CODE = 401;
/**
* Interceptor to add Authorization header and handle 401 errors
*/
export const authInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) => {
const authStore = inject(AuthStore);
const notificationsStore = inject(NotificationsStore);
const router = inject(Router);
// Add Authorization header
const accessToken = authStore.accessToken();
const authorizationHeader = accessToken ? `Bearer ${accessToken}` : '';
req = req.clone({
setHeaders: {
Authorization: authorizationHeader,
},
});
// Handle errors
return next(req).pipe(
catchError((error) => {
if (error.status === AUTH_ERROR_CODE) {
authStore.setState(NULL_USER_ACCESS_TOKEN);
router.navigate(['/auth', 'login']);
}
notificationsStore.addNotification({
message: error.message,
type: 'error'
});
return throwError(() => error);
}),
);
};
Registering interceptors
Register interceptors in your app configuration:
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './core/providers/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient(
withInterceptors([authInterceptor])
),
]
};
Creating a logging interceptor
Create the interceptor
src/app/core/providers/logging.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { tap } from 'rxjs';
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const started = Date.now();
console.log(`[HTTP] ${req.method} ${req.url}`);
return next(req).pipe(
tap({
next: (event) => {
const elapsed = Date.now() - started;
console.log(`[HTTP] ${req.method} ${req.url} completed in ${elapsed}ms`);
},
error: (error) => {
const elapsed = Date.now() - started;
console.error(`[HTTP] ${req.method} ${req.url} failed in ${elapsed}ms`, error);
}
})
);
};
Register the interceptor
import { loggingInterceptor } from './core/providers/logging.interceptor';
import { authInterceptor } from './core/providers/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
loggingInterceptor, // Runs first
authInterceptor, // Runs second
])
),
]
};
Interceptors run in the order they’re registered. The first interceptor in the array runs first.
Creating a retry interceptor
Handle temporary failures with automatic retry:
src/app/core/providers/retry.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { retry } from 'rxjs';
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
// Only retry GET requests
if (req.method === 'GET') {
return next(req).pipe(
retry({
count: 3,
delay: 1000,
})
);
}
return next(req);
};
Creating a cache interceptor
Cache GET requests to reduce network calls:
src/app/core/providers/cache.interceptor.ts
import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { of, tap } from 'rxjs';
const cache = new Map<string, HttpResponse<unknown>>();
export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
// Only cache GET requests
if (req.method !== 'GET') {
return next(req);
}
const cachedResponse = cache.get(req.url);
if (cachedResponse) {
console.log(`[Cache] Returning cached response for ${req.url}`);
return of(cachedResponse);
}
return next(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
cache.set(req.url, event);
}
})
);
};
Conditional interceptor logic
Skip interceptor logic for specific requests:
src/app/core/providers/conditional.interceptor.ts
import { HttpInterceptorFn, HttpContextToken } from '@angular/common/http';
export const SKIP_AUTH = new HttpContextToken<boolean>(() => false);
export const conditionalInterceptor: HttpInterceptorFn = (req, next) => {
// Skip auth for requests with SKIP_AUTH context
if (req.context.get(SKIP_AUTH)) {
return next(req);
}
// Add auth header
const authReq = req.clone({
setHeaders: { Authorization: 'Bearer token' }
});
return next(authReq);
};
Usage:
import { HttpClient, HttpContext } from '@angular/common/http';
import { SKIP_AUTH } from './core/providers/conditional.interceptor';
export class PublicApiService {
constructor(private http: HttpClient) {}
getPublicData() {
return this.http.get('/api/public', {
context: new HttpContext().set(SKIP_AUTH, true)
});
}
}
Best practices
- Use functional guards - Prefer
CanActivateFn over class-based guards
- Use functional interceptors - Prefer
HttpInterceptorFn over class-based interceptors
- Inject dependencies - Use
inject() function inside guards and interceptors
- Return UrlTree for redirects - Use
router.createUrlTree() instead of imperative navigation
- Handle errors gracefully - Use RxJS operators like
catchError in interceptors
- Order matters - Register interceptors in the correct order for your use case
- Use context tokens - Skip interceptor logic for specific requests when needed
- Keep guards simple - Guards should only check conditions, not perform complex operations