AngularJSRoute Guards

Route Guards in Angular

Route guards are functions (or classes) that Angular's router calls before activating, deactivating, or loading a route. They let you:

  • Protect pages from unauthenticated users
  • Prevent leaving a form with unsaved changes
  • Control whether a lazy-loaded module is downloaded at all
  • Pre-fetch data before a component renders (resolvers)

Angular 14.2+ prefers functional guards — plain functions that return boolean, UrlTree, or an Observable/Promise of either. Class-based guards still work but the functional style is simpler and more testable.

Guard Types Overview

Guard type

Route property

Purpose

canActivate

canActivate

Prevent entering a route

canActivateChild

canActivateChild

Prevent entering any child route

canDeactivate

canDeactivate

Prevent leaving a route (unsaved changes)

canMatch

canMatch

Decide whether a route should even be considered

canLoad (deprecated)

canLoad

Prevent loading a lazy module (use canMatch)

resolve

resolve

Pre-fetch data before the route activates

canActivate: Protecting a Route

The most common guard. Returns true to allow navigation, false to block it, or a UrlTree to redirect.

TS
// auth.guard.ts — functional guard
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

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

  if (auth.isLoggedIn()) {
    return true;                       // allow navigation
  }

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

TS
// app.routes.ts — apply the guard
import { authGuard } from './guards/auth.guard';

export const routes: Routes = [
  { path: 'login', component: LoginComponent },
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [authGuard],
  },
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [authGuard, adminGuard],  // multiple guards — ALL must pass
  },
];
Role-Based Guard

TS
// role.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, ActivatedRouteSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
  const auth   = inject(AuthService);
  const router = inject(Router);

  const requiredRole: string = route.data['role'];
  const userRole = auth.currentUser()?.role;

  if (userRole === requiredRole) return true;

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

// app.routes.ts
{
  path: 'admin',
  component: AdminComponent,
  canActivate: [authGuard, roleGuard],
  data: { role: 'ADMIN' },
}
canActivateChild: Protecting All Child Routes

Apply a guard to all children of a parent route in one place rather than repeating it on every child.

TS
// auth.guard.ts — the same function works as canActivateChild too
import { CanActivateChildFn } from '@angular/router';

export const authGuard: CanActivateChildFn = (childRoute, state) => {
  // same logic as canActivate
  const auth = inject(AuthService);
  return auth.isLoggedIn() || inject(Router).createUrlTree(['/login']);
};

// app.routes.ts
{
  path: 'admin',
  component: AdminShellComponent,
  canActivateChild: [authGuard],    // applies to ALL children
  children: [
    { path: '',       component: AdminDashboardComponent },
    { path: 'users',  component: AdminUsersComponent },
    { path: 'config', component: AdminConfigComponent },
  ],
}
canDeactivate: Preventing Accidental Navigation Away

Warn users when they try to leave a form with unsaved changes.

TS
// unsaved-changes.guard.ts
import { CanDeactivateFn } from '@angular/router';

// The guard can call a method on the component
export interface HasUnsavedChanges {
  hasUnsavedChanges(): boolean;
}

export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
  if (component.hasUnsavedChanges()) {
    return confirm('You have unsaved changes. Leave anyway?');
  }
  return true;
};

TS
// edit-profile.component.ts
import { Component } from '@angular/core';
import { HasUnsavedChanges } from './unsaved-changes.guard';
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';

@Component({ standalone: true, imports: [ReactiveFormsModule], template: `...` })
export class EditProfileComponent implements HasUnsavedChanges {
  private fb = inject(FormBuilder);

  form = this.fb.group({ name: [''], bio: [''] });

  hasUnsavedChanges(): boolean {
    return this.form.dirty;
  }
}

// app.routes.ts
{
  path: 'profile/edit',
  component: EditProfileComponent,
  canDeactivate: [unsavedChangesGuard],
}
Note
In modern browsers, confirm() dialogs are blocked in some contexts. For production apps, implement a custom modal-based confirmation instead of relying on the browser's native dialog.
canMatch: Preventing Lazy Chunk Download

canMatch runs before the route is even considered — meaning Angular won't download the lazy chunk for an unauthorised user. This is more secure than canActivate, which runs after the chunk is already downloaded.

TS
// admin-match.guard.ts
import { inject } from '@angular/core';
import { CanMatchFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const adminMatchGuard: CanMatchFn = () => {
  const auth = inject(AuthService);
  if (auth.hasRole('ADMIN')) return true;

  inject(Router).navigate(['/forbidden']);
  return false;
};

// app.routes.ts
{
  path: 'admin',
  canMatch: [adminMatchGuard],           // chunk NOT downloaded for non-admins
  loadChildren: () =>
    import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
}
Tip
Prefer canMatch over the deprecated canLoad for all new code. canMatch also works with non-lazy routes and integrates cleanly with the functional guard style.
Resolve: Pre-fetching Data

A resolver pre-fetches data before the component renders. The user stays on the current page (with a loading indicator) until the data is ready, then Angular activates the route.

TS
// user.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn, ActivatedRouteSnapshot } from '@angular/router';
import { UserService } from './user.service';
import { User } from './user.model';

export const userResolver: ResolveFn<User> = (route: ActivatedRouteSnapshot) => {
  const id = route.paramMap.get('id')!;
  return inject(UserService).getUser(+id);
  // Returns an Observable — Angular subscribes and waits for it
};

// app.routes.ts
{
  path: 'users/:id',
  component: UserDetailComponent,
  resolve: { user: userResolver },
}

// user-detail.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({ standalone: true, template: `<h1>{{ user.name }}</h1>` })
export class UserDetailComponent {
  private route = inject(ActivatedRoute);
  user = this.route.snapshot.data['user'];   // resolved before component renders
}
Async Guards (Observable/Promise)

Guards can return Observable<boolean | UrlTree> or Promise<boolean | UrlTree> for asynchronous checks — token verification, permission API calls, etc.

TS
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
import { map, take } from 'rxjs/operators';

export const authGuard: CanActivateFn = () => {
  const auth   = inject(AuthService);
  const router = inject(Router);

  return auth.isAuthenticated$.pipe(   // Observable<boolean>
    take(1),
    map(isAuth => isAuth || router.createUrlTree(['/login']))
  );
};
Warning
Always use take(1) or first() when returning an Observable from a guard. Angular will wait indefinitely if the observable never completes, blocking navigation forever.
Class-Based Guards (Legacy)

Older codebases use class-based guards that implement CanActivate, CanDeactivate, etc. They still work in modern Angular but are not recommended for new code.

TS
// Class-based guard (legacy style — still supported)
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private auth: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    if (this.auth.isLoggedIn()) return true;
    this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
    return false;
  }
}
Combining Guards

TS
// Multiple guards — ALL must return true for navigation to proceed
{
  path: 'finance/reports',
  component: FinanceReportsComponent,
  canActivate: [authGuard, roleGuard, featureFlagGuard],
  data: { role: 'FINANCE_MANAGER', feature: 'advanced-reports' },
}

// Guard order matters — they run left to right
// If authGuard redirects to /login, roleGuard never runs
Testing Guards

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

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

  beforeEach(() => {
    authService = jasmine.createSpyObj('AuthService', ['isLoggedIn']);
    TestBed.configureTestingModule({
      imports: [RouterTestingModule],
      providers: [{ provide: AuthService, useValue: authService }],
    });
    router = TestBed.inject(Router);
  });

  it('allows navigation when logged in', () => {
    authService.isLoggedIn.and.returnValue(true);
    const result = TestBed.runInInjectionContext(() =>
      authGuard({} as any, { url: '/dashboard' } as any)
    );
    expect(result).toBe(true);
  });

  it('redirects to login when not logged in', () => {
    authService.isLoggedIn.and.returnValue(false);
    const result = TestBed.runInInjectionContext(() =>
      authGuard({} as any, { url: '/dashboard' } as any)
    );
    expect(result).toEqual(router.createUrlTree(['/login'], { queryParams: { returnUrl: '/dashboard' } }));
  });
});
Guard Best Practices
  • Use functional guards (plain functions) for all new code — they are simpler and more testable

  • Return a UrlTree for redirects rather than calling router.navigate() — it is cleaner and composable

  • Use canMatch instead of canLoad to prevent downloading lazy chunks for unauthorised users

  • Apply canActivateChild to a parent route to protect all children in one place

  • Keep guards thin — delegate complex logic to injectable services

  • Always use take(1)/first() on Observables to ensure they complete

  • Test guards with TestBed.runInInjectionContext() to properly inject dependencies

Summary: Route guards are functions that run before Angular activates, loads, or leaves a route. Use canActivate to protect routes, canDeactivate to prevent accidental navigation, canMatch to block lazy chunk downloads, and resolve to pre-fetch data. The functional guard style (Angular 14.2+) is the recommended approach for all new code.