Routing & Navigation

Angular Route Guards

Protect routes with Angular guards — canActivate, canDeactivate, resolve, and functional guards for authentication and authorization.

What Are Route Guards?

Route guards are functions that control whether a user can navigate to or away from a route. They run before the route is activated and can:

  • Prevent unauthorized access
  • Redirect to a login page
  • Confirm before leaving unsaved changes
  • Pre-fetch data before a route loads

Types of Route Guards

GuardWhen It RunsUse Case
canActivateBefore navigating to a routeAuthentication check
canActivateChildBefore navigating to child routesProtect all admin children
canDeactivateBefore leaving a routeUnsaved changes warning
canMatchBefore loading route configFeature flags, AB testing
resolveBefore route activatesPre-fetch data

Functional Guards (Modern — Angular 15+)

canActivate — Authentication Guard

typescript
// guards/auth.guard.ts
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';

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

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

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

Usage in Routes

typescript
const routes: Routes = [
  { path: 'login', component: LoginComponent },
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [authGuard],
  },
  {
    path: 'admin',
    component: AdminLayoutComponent,
    canActivate: [authGuard, roleGuard],
    children: [
      { path: '', component: AdminDashboardComponent },
      { path: 'users', component: AdminUsersComponent },
    ],
  },
];

Role-Based Guard

typescript
// guards/role.guard.ts
export const roleGuard: CanActivateFn = (route) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  const requiredRoles = route.data['roles'] as string[];
  const userRole = authService.getUserRole();

  if (requiredRoles.includes(userRole)) {
    return true;
  }

  router.navigate(['/unauthorized']);
  return false;
};
typescript
// Route with role requirement
{
  path: 'admin',
  component: AdminComponent,
  canActivate: [authGuard, roleGuard],
  data: { roles: ['admin', 'super-admin'] },
}

canDeactivate — Unsaved Changes Guard

Prevent users from accidentally losing unsaved work:

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

export interface HasUnsavedChanges {
  hasUnsavedChanges(): boolean;
}

export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
  if (component.hasUnsavedChanges()) {
    return confirm('You have unsaved changes. Do you really want to leave?');
  }
  return true;
};
typescript
// The component implements the interface
@Component({ /* ... */ })
export class EditProfileComponent implements HasUnsavedChanges {
  originalData = '';
  currentData = '';

  hasUnsavedChanges(): boolean {
    return this.originalData !== this.currentData;
  }
}
typescript
// Route configuration
{
  path: 'profile/edit',
  component: EditProfileComponent,
  canActivate: [authGuard],
  canDeactivate: [unsavedChangesGuard],
}

Route Resolvers

Resolvers pre-fetch data before a route activates, so the component receives the data immediately:

Functional Resolver

typescript
// resolvers/user.resolver.ts
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { UserService } from '../services/user.service';

export const userResolver: ResolveFn<User> = (route) => {
  const userService = inject(UserService);
  const userId = route.params['id'];
  return userService.getUserById(userId);
};

export const userListResolver: ResolveFn<User[]> = () => {
  return inject(UserService).getUsers();
};

Using Resolvers in Routes

typescript
const routes: Routes = [
  {
    path: 'users',
    component: UserListComponent,
    resolve: { users: userListResolver },
  },
  {
    path: 'users/:id',
    component: UserDetailComponent,
    resolve: { user: userResolver },
  },
];

Accessing Resolved Data

typescript
@Component({
  selector: 'app-user-detail',
  standalone: true,
  template: `
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  `,
})
export class UserDetailComponent implements OnInit {
  private route = inject(ActivatedRoute);
  user!: User;

  ngOnInit() {
    this.user = this.route.snapshot.data['user'];

    // Or reactively
    this.route.data.subscribe(data => {
      this.user = data['user'];
    });
  }
}

canMatch — Feature Flags

Control whether a route even matches based on conditions:

typescript
export const featureFlagGuard: CanMatchFn = (route) => {
  const featureService = inject(FeatureService);
  const feature = route.data?.['feature'];
  return featureService.isEnabled(feature);
};

const routes: Routes = [
  {
    path: 'new-dashboard',
    component: NewDashboardComponent,
    canMatch: [featureFlagGuard],
    data: { feature: 'new-dashboard' },
  },
  {
    path: 'new-dashboard',
    component: OldDashboardComponent, // Fallback
  },
];

Guard with Async Logic

Guards can return Observables or Promises:

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

  return authService.checkAuth().pipe(
    map(isAuthenticated => {
      if (isAuthenticated) return true;
      return router.createUrlTree(['/login']);
    }),
    catchError(() => {
      return of(router.createUrlTree(['/login']));
    }),
  );
};

Complete Guard Example

typescript
// A comprehensive auth guard
export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  // Check if logged in
  if (!authService.isLoggedIn()) {
    router.navigate(['/login'], {
      queryParams: { returnUrl: state.url },
    });
    return false;
  }

  // Check if token is expired
  if (authService.isTokenExpired()) {
    authService.logout();
    router.navigate(['/login'], {
      queryParams: { reason: 'expired' },
    });
    return false;
  }

  // Check role requirements
  const requiredRoles = route.data['roles'] as string[] | undefined;
  if (requiredRoles && !requiredRoles.includes(authService.getUserRole())) {
    router.navigate(['/unauthorized']);
    return false;
  }

  return true;
};

Best Practices

  1. Use functional guards — they are simpler and tree-shakable
  2. Combine guards with the canActivate array for layered security
  3. Return UrlTree instead of calling router.navigate() for cleaner redirects
  4. Keep guards focused — one guard per concern (auth, roles, permissions)
  5. Use resolvers to prevent "loading" flickers on route change

Next Steps

For large applications, loading everything upfront is inefficient. Next, we'll explore lazy loading — how to split your app into chunks that load on demand for better performance.