DEV Community

Cover image for Everything you need to know about route Guard in Angular
thomas for This is Angular

Posted on • Updated on • Originally published at Medium

Everything you need to know about route Guard in Angular

Routing is a significant aspect of any SPA application, and protecting these routes is often necessary. We may want to guard our routes for permission access or to prevent users from exiting a route by mistake if a form has not been submitted correctly.

Angular provides a set of built-in guards that can be easily used for various use cases.

In this article, I will demonstrate and explain each of the built-in guards provided and show you how to use them with some common examples.

CanActivate

This is the most widely used guard. The canActivate guard is a route guard that allows or denies access to a route based on the logic specified in the guard. The method implemented in the guard returns a boolean , a UrlTree, a Promise<boolean | UrlTree> or an Observable<boolean | UrlTree>.

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree
Enter fullscreen mode Exit fullscreen mode

When the guard is used on a route, the router will call the method defined in the guard before navigating to the route. If the method returns true, the navigation will proceed. If it returns false, the navigation will be canceled and the user will stay on the current route. If the method returns a promise or an observable, the router will wait for it to resolve before proceeding with the navigation. If the method return a UrlTree , the navigation will be canceled and the new navigation will be executed.

Example:

@Injectable({ providedIn: 'root' })
export class PermissionsService {
  private user = getUser();

  isAdmin(isAdmin: boolean) {
    return isAdmin ? user.isAdmin : false;
  }
}

@Injectable({ providedIn: 'root' })
export class IsAdminGuard implements CanActivate {
  private permission = inject(PermissionsService);

  canActivate(route: ActivatedRouteSnapshot) {
      const isAdmin: boolean = route.data?.['isAdmin'] ?? false;
      return this.permission.isAdmin(isAdmin);
  }
}

export const APP_ROUTES: [{
  path: 'dashboard',
  canActivate: [IsAdminGuard],
  data: {
    isAdmin: true,
  },
  loadComponent: () => import('./dashboard/admin.component'),
}]

Enter fullscreen mode Exit fullscreen mode

This example illustrates the typical way of implementing a guard. We declare a class as a service that implements the CanActivate interface. In this scenario, we are checking if the user is an admin, as this route can only be accessed by admin users.

It is possible to pass data to our guard by setting properties inside the data property of the Route object.

Warning: implementing guard as injectable services is going to be deprecated in v15.2 and removed in v17

Injectable Class and InjectionToken -based guards are less configurable and reusable and require more boilerplate code. Additionally they cannot be inlined making them less powerful and more cumbersome.

Deprecate class and `InjectionToken` guards and resolvers #47924

Class and InjectionToken-based guards and resolvers are not as configurable, are less re-usable, require more boilerplate, cannot be defined inline with the route, and require more in-depth knowledge of Angular features (Injectable/providers). In short, they're less powerful and more cumbersome.

In addition, continued support increases the API surface which in turn increases bundle size, code complexity, the learning curve and API surface to teach, maintenance cost, and cognitive load (needing to grok several different types of information in a single place).

Lastly, supporting only the CanXFn types for guards and ResolveFn type for resolvers in the Route interface will enable better code completion and integration with TypeScript. For example, when writing an inline functional resolver today, the function is typed as any and does not provide completions for the ResolveFn parameters. By restricting the type to only ResolveFn, in the example below TypeScript would be able to correctly identify the route parameter as ActivatedRouteSnapshot and when authoring the inline route, the language service would be able to autocomplete the function parameters.

const userRoute: Route = {
  path: 'user/:id',
  resolve: {
    "user": (route) => inject(UserService).getUser(route.params['id']);
  }
};

Importantly, this deprecation only affects the support for class and InjectionToken guards at the Route definition. Injectable classes and InjectionToken providers are not being deprecated in the general sense. Functional guards are robust enough to even support the existing class-based guards through a transform:

function mapToCanMatch(providers: Array<Type<{canMatch: CanMatchFn}>>): CanMatchFn[] {
  return providers.map(provider => (...params) => inject(provider).canMatch(...params));
}
const route = {
  path: 'admin',
  canMatch: mapToCanMatch([AdminGuard]),
};

With regards to tests, because of the ability to map Injectable classes to guard functions as outlined above, nothing needs to change if projects prefer testing guards the way they do today. Functional guards can also be written in a way that's either testable with runInContext or by passing mock implementations of dependencies. For example:

export function myGuardWithMockableDeps(
  dep1 = inject(MyService),
  dep2 = inject(MyService2),
  dep3 = inject(MyService3),
) { }

const route = {
  path: 'admin',
  canActivate: [() => myGuardWithMockableDeps()]
}

// test file
const guardResultWithMockDeps = myGuardWithMockableDeps(mockService1, mockService2, mockService3);
const guardResultWithRealDeps = TestBed.inject(EnvironmentInjector).runInContext(myGuardWithMockableDeps);

If you still want to do it this way or for backward compatibility, you will need to create a function to inject your service, like this:

function mapToActivate(providers: Array<Type<{canActivate: CanActivateFn}>>): CanActivateFn[] {
  return providers.map(provider => (...params) => inject(provider).canActivate(...params));
}
const route = {
  path: 'admin',
  canActivate: mapToActivate([IsAdminGuard]),
};
Enter fullscreen mode Exit fullscreen mode

The new way:

@Injectable({ providedIn: 'root' })
export class PermissionsService {
  isAdmin(isAdmin: boolean) {
    return isAdmin;
  }
}

export const canActivate = (isAdmin: boolean, permissionService = inject(PermissionsService)) => permissionService.isAdmin(isAdmin);

export const APP_ROUTES: [{
  path: 'dashboard',
  canActivate: [() => canActivate(true)],
  loadComponent: () => import('./dashboard/admin.component'),
 }]
Enter fullscreen mode Exit fullscreen mode

It feels better, doesn't it ? With less boilerplate and more explicit code (We don't need to set some properties on the Route data attribute that are often forgotten about in the previous approach)


For the purpose of this article, all others examples will be implemented using the new approach.

CanMatch

The CanMatch guard is a new feature that was introduced in Angular v14.2. It will activate the route and load the lazy-loaded component if all guards return true, otherwise it will navigate to the next route with the same name.

Warning: It's important to note that ONE route has to match otherwise you will encounter an error in your console.

ERROR Error: Uncaught (in promise): Error: NG04002: Cannot match any routes. 
URL Segment: 'dashboard'
Enter fullscreen mode Exit fullscreen mode

Example:

@Injectable({ providedIn: 'root' })
export class PermissionService {
  isAllowed(permissions: Permission[]) {
    const user = ...
    return permissions.includes(user.permission);
  }
}

export type Permission = 'ADMIN' | 'USER' | 'MANAGER';

export const canMatch = (permissions: Permission[], permissionService = inject(PermissionsService)) =>
  permissionService.isAllowed(permissions);

export const APP_ROUTES: [
  {
    path: 'dashboard',
    canMatch: [() => canMatch(['ADMIN'])],
    loadComponent: () => import('./dashboard/admin.component'),
  },
  {
    path: 'dashboard',
    canMatch: [() => canMatch(['MANAGER'])],
    loadComponent: () => import('./dashboard/manager.component'),
  },
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/everyone.component'),
  }
]
Enter fullscreen mode Exit fullscreen mode

If we want to navigate to the /dashboard route, it will first check if user has the ADMIN permission, if it does, the router will load the AdminComponent otherwise the router will continue to the next possible route, and so on. We can set a fallback route specifically for this dashboard navigator, but we can also set a catch-all routes ** which will catch all failed navigations:

{
  path: '**',
  loadComponent: () => import('./not-found.component'),
}
Enter fullscreen mode Exit fullscreen mode

Note: This guard can also return a UrlTree , which will cancel the previous navigation and trigger a new navigation.

@Injectable({ providedIn: 'root' })
export class PermissionService {
  constructor(private router: Router) {}

  isAllowed(permissions: Permission[]) {
    if(!user) {
      return this.router.parseUrl('no-user');
    }
    // check permissions
  }
}
Enter fullscreen mode Exit fullscreen mode

CanActivateChild

This is quite similar to CanActivate and is often misunderstood.

To help clarify the differences, here is an example:

export const APP_ROUTES = [
  {
    path: 'dashboard',
    canActivate: [() => true],
    canActivateChild: [() => true],
    loadComponent: () => import('./dashboard/no-user.component'),
    loadChildren: () => import('./child-routes').then((m) => m.CHILDREN_ROUTE),
  }
]

// inside child-routes
export const CHILDREN_ROUTE = [
  {
    path: 'manager',
    loadComponent: () => import('./dashboard/manager.component'),
  },
  {
    path: 'client',
    loadComponent: () => import('./dashboard/client.component'),
  },
];
Enter fullscreen mode Exit fullscreen mode

The key differences between the two are: 

  • If we navigate from the root / to /dashboard/manager route, both CanActivate and CanActivateChild guards will be executed. However if we navigate between child components (from /dashboard/manager to /dashboard/client), only CanActivateChild will be executed. CanActivate is only executed when the parent component is not yet created.

  • If we navigate only to the parent component, only CanActivate will be executed

  • If we navigate to one child, and one of the guard inside CanActivateChild return false, the entire route is canceled and the parent will not be created.

  • CanActivate is executed before CanActivateChild . If CanActivate return false, CanActivateChild will not be executed.

  • We can replace CanActivateChild which CanActivate on every child route. 

CanDeactivate

The CanDeactivate guard is used to control the navigation away from a route. It allows you to prevent the user from leaving a route or a component until some condition is met, or to prompt the user for confirmation before navigating away.

Note: It's commonly used when working with forms to prevent the user from navigating away if the form has been modified but not yet submitted. We can display a modal dialog with a warning message to notify the user of any unsaved changes.

Example:

export interface DeactivationGuarded {
  canDeactivate(): Observable<boolean> | Promise<boolean> | boolean;
}

@Component({
  standalone: true,
  imports: [RouterLink, ButtonComponent],
  template: `<button app-button routerLink="/">Logout</button>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class NoUserDashboardComponent implements DeactivationGuarded {
  canDeactivate(): boolean | Observable<boolean> | Promise<boolean> {
    return false;
  }
}

export const APP_ROUTES = [
  {
    path: 'dashboard',
    canDeactivate: [(comp: DeactivationGuarded) => comp.canDeactivate()],
    loadComponent: () => import('./dashboard/no-user.component'),
  }
]
Enter fullscreen mode Exit fullscreen mode
  • CanDeactivate takes the component associated with the route and injects it into the function. 

Note: Like any guard, it can return a UrlTree to navigate to trigger a new navigation to a different route.

CanLoad

CanLoad is a guard that is often used with CanActivate . It loads the lazy-loaded component is the guard returns true.

Note: This guard has been deprecated in Angular v.15.1 and has been replaced by CanMatch

Tips/ Tricks

Chaining guards

A guard property is of type Array which means multiple guards can be chained to a specific route.

  • All guards will be executed in the specified order
  • If one guard return false, the navigation will be cancelled
  • If the first guard returns false, the other guards in the array will not be executed
  • If one guard returns a UrlTree , the following guards will not be executed and the navigation will be rerouted.
export const APP_ROUTES = [
  {
    path: 'dashboard',
    canActivate: [() => true, () => false, () => true],
    loadComponent: () => import('./dashboard/no-user.component'),
  }
]
Enter fullscreen mode Exit fullscreen mode

In this example dashboard route will be canceled, and the last function will not be executed

Routing to children component depending on parameters

Given the following example:

export const CHILDREN_ROUTE = [
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'compA'
  },
  {
    path: 'compA',
    loadComponent: () => import('./dashboard/comp-a.component'),
  },
  {
    path: 'compB',
    loadComponent: () => import('./dashboard/comp-b.component'),
  },
];

export const APP_ROUTES = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/no-user.component'),
    loadChildren: () => import('./child_routes').then((m) => m.CHILDREN_ROUTE),
  }
]
Enter fullscreen mode Exit fullscreen mode

When we navigate to the dashboard , we will be redirected to dashboard/compA . But if we need to redirect to either compA or compB depending on an external parameter (such as user permission or if a specific action has been taken, … ), we can "trick" our routing with a guard that returns a UrlTree to redirect to the correct URL based on certain conditions.

export const redirectTo = (router = inject(Router), userStore = inject(UserStore)) => {
  return userStore.hasDoneAction$.pipe(
    mergeMap((hasDoneAction) =>
      iif(
        () => hasDoneAction,
        of(router.createUrlTree(['dashboard', 'compA'])),
        of(router.createUrlTree(['dashboard', 'compB']))
      )
    )
  );
};

export const CHILDREN_ROUTE: Route[] = [
  {
    path: '',
    pathMatch: 'full',
    children: [],
    canActivate: [() => redirectTo()],
  },
  {
    path: 'compA',
    loadComponent: () => import('./dashboard/comp-a.component'),
  },
  {
    path: 'compB',
    loadComponent: () => import('./dashboard/comp-b.component'),
  },
];
Enter fullscreen mode Exit fullscreen mode

Note: The children property with an empty array must be provided because each route definition must include at least one property among component , loadComponent , redirectTo , children or loadChildren


That's it for this article! You should now have no excuses not to guard your route properly.

I hope you learned new Angular concept. If you liked it, you can find me on Twitter or Github.

👉 If you want to accelerate your Angular and Nx learning journey, come and check out Angular challenges.

Top comments (0)