Skip to main content
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:
src/app/app.routes.ts
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

1

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']);
  };
};
2

Use the parameterized guard

src/app/app.routes.ts
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:
src/app/app.config.ts
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

1

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);
      }
    })
  );
};
2

Register the interceptor

src/app/app.config.ts
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

  1. Use functional guards - Prefer CanActivateFn over class-based guards
  2. Use functional interceptors - Prefer HttpInterceptorFn over class-based interceptors
  3. Inject dependencies - Use inject() function inside guards and interceptors
  4. Return UrlTree for redirects - Use router.createUrlTree() instead of imperative navigation
  5. Handle errors gracefully - Use RxJS operators like catchError in interceptors
  6. Order matters - Register interceptors in the correct order for your use case
  7. Use context tokens - Skip interceptor logic for specific requests when needed
  8. Keep guards simple - Guards should only check conditions, not perform complex operations

Build docs developers (and LLMs) love