Skip to main content

Route Guards

Route guards are functions or classes that control navigation in Angular applications. They can prevent navigation, redirect to different routes, or allow navigation to proceed based on custom logic.

Import

import {
  CanActivateFn,
  CanActivateChildFn,
  CanDeactivateFn,
  CanMatchFn,
  CanLoadFn,
  GuardResult
} from '@angular/router';

Guard Types

Angular provides several types of guards for different navigation scenarios:

CanActivate

Controls if a route can be activated

CanActivateChild

Controls if child routes can be activated

CanDeactivate

Controls if a route can be deactivated

CanMatch

Controls if a route can be matched
Modern Angular applications use functional guards (functions) instead of class-based guards. Functional guards are more concise and leverage dependency injection through the inject() function.

Guard Result Types

GuardResult

type GuardResult = boolean | UrlTree | RedirectCommand;
All guards return a GuardResult, which can be:
  • true - Allow navigation to proceed
  • false - Cancel navigation
  • UrlTree - Redirect to a different URL
  • RedirectCommand - Redirect with navigation options

MaybeAsync

type MaybeAsync<T> = T | Observable<T> | Promise<T>;
Guards can return synchronous values or async values (Observable or Promise).

CanActivate

CanActivateFn

type CanActivateFn = (
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot
) => MaybeAsync<GuardResult>;
Determines if a route can be activated. Used to protect routes that require authentication, authorization, or other preconditions. Parameters:
route
ActivatedRouteSnapshot
required
The activated route snapshot containing route parameters, data, and configuration.
state
RouterStateSnapshot
required
The router state snapshot containing the target URL and routing tree.
Returns: true to allow, false to cancel, or UrlTree/RedirectCommand to redirect. Examples:
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

// Basic authentication guard
export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

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

  // Redirect to login page with return URL
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url }
  });
};

// Role-based authorization guard
export const adminGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.hasRole('admin')) {
    return true;
  }

  // Redirect to unauthorized page
  return router.createUrlTree(['/unauthorized']);
};

// Async guard with Observable
export const permissionGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const requiredPermission = route.data['permission'];

  return authService.hasPermission(requiredPermission).pipe(
    map(hasPermission => hasPermission || router.createUrlTree(['/unauthorized']))
  );
};

// Guard with route parameter validation
export const userAccessGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const userId = route.params['id'];
  const currentUserId = authService.getCurrentUserId();

  // Users can only access their own profile
  if (userId === currentUserId || authService.hasRole('admin')) {
    return true;
  }

  return false;
};
Route Configuration:
import { Routes } from '@angular/router';

const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [authGuard]
  },
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [authGuard, adminGuard] // Multiple guards
  },
  {
    path: 'users/:id',
    component: UserProfileComponent,
    canActivate: [authGuard, userAccessGuard],
    data: { permission: 'users:read' }
  }
];

CanActivate Interface (Deprecated)

interface CanActivate {
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): MaybeAsync<GuardResult>;
}
Class-based guards using CanActivate interface are deprecated. Use functional guards (CanActivateFn) instead.

CanActivateChild

CanActivateChildFn

type CanActivateChildFn = (
  childRoute: ActivatedRouteSnapshot,
  state: RouterStateSnapshot
) => MaybeAsync<GuardResult>;
Determines if child routes can be activated. Applied to parent routes to protect all child routes. Example:
import { inject } from '@angular/core';
import { CanActivateChildFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

// Protect all child routes
export const adminChildGuard: CanActivateChildFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.hasRole('admin')) {
    return true;
  }

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

// Check specific permissions for child routes
export const moduleAccessGuard: CanActivateChildFn = (route, state) => {
  const authService = inject(AuthService);
  const requiredModule = route.data['module'];

  if (authService.hasModuleAccess(requiredModule)) {
    return true;
  }

  return false;
};
Route Configuration:
const routes: Routes = [
  {
    path: 'admin',
    component: AdminLayoutComponent,
    canActivateChild: [adminChildGuard], // Applied to all children
    children: [
      { path: 'users', component: UsersComponent },
      { path: 'settings', component: SettingsComponent },
      { path: 'reports', component: ReportsComponent }
    ]
  }
];

CanDeactivate

CanDeactivateFn

type CanDeactivateFn<T> = (
  component: T,
  currentRoute: ActivatedRouteSnapshot,
  currentState: RouterStateSnapshot,
  nextState: RouterStateSnapshot
) => MaybeAsync<GuardResult>;
Determines if a route can be deactivated. Commonly used to prevent navigation away from pages with unsaved changes. Parameters:
component
T
required
The component instance being deactivated.
currentRoute
ActivatedRouteSnapshot
required
The current activated route.
currentState
RouterStateSnapshot
required
The current router state.
nextState
RouterStateSnapshot
required
The target router state.
Examples:
import { CanDeactivateFn } from '@angular/router';

// Define interface for components with unsaved changes
export interface CanComponentDeactivate {
  canDeactivate: () => boolean | Promise<boolean>;
}

// Guard for unsaved changes
export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = (
  component,
  currentRoute,
  currentState,
  nextState
) => {
  // If component implements canDeactivate, call it
  if (component.canDeactivate) {
    return component.canDeactivate();
  }
  return true;
};

// Guard with user confirmation dialog
export const confirmLeaveGuard: CanDeactivateFn<any> = (
  component,
  currentRoute,
  currentState,
  nextState
) => {
  if (component.hasUnsavedChanges?.()) {
    return confirm('You have unsaved changes. Do you really want to leave?');
  }
  return true;
};

// Async guard with custom dialog service
export const asyncConfirmGuard: CanDeactivateFn<any> = async (
  component,
  currentRoute,
  currentState,
  nextState
) => {
  if (!component.hasUnsavedChanges?.()) {
    return true;
  }

  const dialogService = inject(DialogService);
  const result = await dialogService.confirm({
    title: 'Unsaved Changes',
    message: 'You have unsaved changes. Do you want to discard them?',
    confirmText: 'Discard',
    cancelText: 'Stay'
  });

  return result;
};
Component Implementation:
import { Component } from '@angular/core';
import { CanComponentDeactivate } from './guards/unsaved-changes.guard';

@Component({
  selector: 'app-edit-form',
  template: `
    <form [formGroup]="form">
      <!-- form fields -->
    </form>
  `
})
export class EditFormComponent implements CanComponentDeactivate {
  form: FormGroup;
  private savedData: any;

  canDeactivate(): boolean {
    // Check if form has unsaved changes
    if (this.form.dirty) {
      return confirm('You have unsaved changes. Are you sure you want to leave?');
    }
    return true;
  }

  hasUnsavedChanges(): boolean {
    return this.form.dirty;
  }
}
Route Configuration:
const routes: Routes = [
  {
    path: 'edit',
    component: EditFormComponent,
    canDeactivate: [unsavedChangesGuard]
  }
];

CanMatch

CanMatchFn

type CanMatchFn = (
  route: Route,
  segments: UrlSegment[],
  currentSnapshot?: PartialMatchRouteSnapshot
) => MaybeAsync<GuardResult>;
Determines if a route configuration can be matched. Used for feature flags, conditional routing, and A/B testing. Parameters:
route
Route
required
The route configuration being evaluated.
segments
UrlSegment[]
required
The URL segments that have not been consumed yet.
currentSnapshot
PartialMatchRouteSnapshot
The current route snapshot up to this point in matching (optional for backward compatibility).
Examples:
import { inject } from '@angular/core';
import { CanMatchFn } from '@angular/router';
import { FeatureFlagService } from './feature-flag.service';

// Feature flag guard
export const featureGuard: CanMatchFn = (route, segments) => {
  const featureFlags = inject(FeatureFlagService);
  const featureName = route.data?.['feature'];

  if (!featureName) {
    return true;
  }

  return featureFlags.isEnabled(featureName);
};

// Environment-based routing
export const productionOnlyGuard: CanMatchFn = () => {
  return environment.production;
};

// User segment guard for A/B testing
export const betaUserGuard: CanMatchFn = (route, segments) => {
  const userService = inject(UserService);
  return userService.isBetaUser();
};

// Mobile/Desktop conditional routing
export const mobileGuard: CanMatchFn = () => {
  const platformService = inject(PlatformService);
  return platformService.isMobile();
};
Route Configuration:
const routes: Routes = [
  // New feature behind feature flag
  {
    path: 'dashboard',
    component: NewDashboardComponent,
    canMatch: [featureGuard],
    data: { feature: 'new-dashboard' }
  },
  // Fallback to old dashboard if feature disabled
  {
    path: 'dashboard',
    component: OldDashboardComponent
  },

  // A/B testing routes
  {
    path: 'home',
    component: HomeVariantAComponent,
    canMatch: [betaUserGuard]
  },
  {
    path: 'home',
    component: HomeVariantBComponent
  },

  // Platform-specific routes
  {
    path: 'editor',
    component: MobileEditorComponent,
    canMatch: [mobileGuard]
  },
  {
    path: 'editor',
    component: DesktopEditorComponent
  }
];
Unlike CanActivate, CanMatch prevents the route from being matched at all. If a canMatch guard returns false, the router continues to the next route configuration. This is ideal for feature flags and conditional routing.

CanLoad (Deprecated)

CanLoadFn

type CanLoadFn = (
  route: Route,
  segments: UrlSegment[]
) => MaybeAsync<GuardResult>;
CanLoad is deprecated in favor of CanMatch. Use CanMatchFn for all new code.

RedirectCommand

RedirectCommand Class

class RedirectCommand {
  constructor(
    readonly redirectTo: UrlTree,
    readonly navigationBehaviorOptions?: NavigationBehaviorOptions
  );
}
Used in guards and resolvers to redirect with specific navigation options. Example:
import { inject } from '@angular/core';
import { CanActivateFn, Router, RedirectCommand } from '@angular/router';
import { AuthService } from './auth.service';

export const authWithRedirectGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

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

  const loginUrl = router.parseUrl('/login');
  return new RedirectCommand(loginUrl, {
    skipLocationChange: true, // Don't update browser URL
    state: { returnUrl: state.url } // Pass data to login page
  });
};

Guard Execution Order

When multiple guards are present, they execute in the following order:
  1. Route matching: canMatch guards
  2. Activation: canActivate guards (from parent to child)
  3. Child activation: canActivateChild guards (from parent to child)
  4. Deactivation: canDeactivate guards (when leaving route)
Example:
const routes: Routes = [
  {
    path: 'admin',
    component: AdminLayoutComponent,
    canMatch: [featureGuard], // 1. Check feature flag
    canActivate: [authGuard], // 2. Check authentication
    canActivateChild: [adminGuard], // 3. Check admin role for children
    children: [
      {
        path: 'settings',
        component: SettingsComponent,
        canActivate: [settingsAccessGuard], // 4. Additional guard for this child
        canDeactivate: [unsavedChangesGuard] // 5. Check when leaving
      }
    ]
  }
];

Common Guard Patterns

Combining Multiple Guards

const routes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [
      authGuard,      // Must be logged in
      adminGuard,     // Must be admin
      licenseGuard    // Must have valid license
    ]
  }
];

Reusable Permission Guard

import { inject } from '@angular/core';
import { CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';

export const permissionGuard = (permission: string): CanActivateFn => {
  return (route, state) => {
    const authService = inject(AuthService);
    return authService.hasPermission(permission);
  };
};

// Usage
const routes: Routes = [
  {
    path: 'users',
    component: UsersComponent,
    canActivate: [permissionGuard('users:read')]
  },
  {
    path: 'users/create',
    component: CreateUserComponent,
    canActivate: [permissionGuard('users:create')]
  }
];

Guard with Loading State

import { inject } from '@angular/core';
import { CanActivateFn } from '@angular/router';
import { LoadingService } from './loading.service';
import { AuthService } from './auth.service';
import { tap, finalize } from 'rxjs/operators';

export const authWithLoadingGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const loadingService = inject(LoadingService);

  loadingService.show();

  return authService.checkAuth().pipe(
    tap(isAuthenticated => {
      if (!isAuthenticated) {
        // Handle redirect
      }
    }),
    finalize(() => loadingService.hide())
  );
};

Guard with Error Handling

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
import { catchError, of } from 'rxjs';

export const safeAuthGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  return authService.checkAuth().pipe(
    catchError(error => {
      console.error('Auth check failed:', error);
      // Redirect to error page or login
      return of(router.createUrlTree(['/error']));
    })
  );
};

Testing Guards

import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { authGuard } from './auth.guard';
import { AuthService } from './auth.service';

describe('authGuard', () => {
  let authService: jasmine.SpyObj<AuthService>;
  let router: jasmine.SpyObj<Router>;

  beforeEach(() => {
    const authServiceSpy = jasmine.createSpyObj('AuthService', ['isLoggedIn']);
    const routerSpy = jasmine.createSpyObj('Router', ['createUrlTree']);

    TestBed.configureTestingModule({
      providers: [
        { provide: AuthService, useValue: authServiceSpy },
        { provide: Router, useValue: routerSpy }
      ]
    });

    authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
    router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
  });

  it('should allow access when user is logged in', () => {
    authService.isLoggedIn.and.returnValue(true);

    const result = TestBed.runInInjectionContext(() =>
      authGuard(null as any, null as any)
    );

    expect(result).toBe(true);
  });

  it('should redirect to login when user is not logged in', () => {
    authService.isLoggedIn.and.returnValue(false);
    const mockUrlTree = {} as any;
    router.createUrlTree.and.returnValue(mockUrlTree);

    const result = TestBed.runInInjectionContext(() =>
      authGuard(null as any, { url: '/admin' } as any)
    );

    expect(result).toBe(mockUrlTree);
    expect(router.createUrlTree).toHaveBeenCalledWith(
      ['/login'],
      { queryParams: { returnUrl: '/admin' } }
    );
  });
});

See Also

Router Class

Router service API

Route Configuration

Configure routes with guards

Route Guards Guide

Complete guide to route guards

Dependency Injection

Using inject() in guards