Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to create a route guard in Angular

Securing application routes and managing navigation access is a fundamental requirement for any professional web application.
With over 25 years of experience in software development and as the creator of CoreUI, I have built and secured hundreds of complex Angular architectures.
From my expertise, the most efficient and modern way to handle this in Angular is by using Functional Route Guards, which were introduced to replace the older, more verbose class-based approach.
This modern pattern leverages the inject function to create lightweight, modular, and highly testable security checks for your routes.

Create a functional route guard by defining a constant of type CanActivateFn and using the inject function to access your authentication services.

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

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

  return authService.isLoggedIn() 
    ? true 
    : router.parseUrl('/login')
}

In this example, the authGuard is a function that takes the current route snapshot and the router state. We use the inject function to retrieve the AuthService and the Router directly inside the function body. If the user is logged in, the guard returns true, allowing navigation to proceed. Otherwise, it uses router.parseUrl to redirect the user to the login page, which is the preferred modern way to handle redirects in Angular guards.

Implementing Basic Authentication Guards

The first step in securing your Angular Dashboard Template is creating a basic guard that checks for an active session. Using functional guards makes this process significantly less complex than previous versions of Angular.

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

export const isAuthenticatedGuard: CanActivateFn = () => {
  const authService = inject(AuthService)
  const router = inject(Router)

  if (authService.isAuthenticated()) {
    return true
  }

  // Redirect to the login page if not authenticated
  return router.parseUrl('/login')
}

This snippet demonstrates the core logic of an authentication guard. By returning a UrlTree (via router.parseUrl), you provide a clear instruction to the router to cancel the current navigation and start a new one to the specified path. This is much cleaner than manually calling router.navigate() inside the guard.

Handling Asynchronous Authorization

Often, checking permissions requires a call to an API or an asynchronous identity provider. Angular route guards natively support returning an Observable or a Promise.

import { inject } from '@angular/core'
import { CanActivateFn, Router } from '@angular/router'
import { map, take } from 'rxjs'
import { UserService } from './user.service'

export const asyncPermissionGuard: CanActivateFn = () => {
  const userService = inject(UserService)
  const router = inject(Router)

  return userService.userProfile$.pipe(
    take(1),
    map(user => {
      if (user && user.hasPermission('admin')) {
        return true
      }
      return router.parseUrl('/unauthorized')
    })
  )
}

When returning an Observable, ensure you use the take(1) operator so the stream completes after the first value is emitted. The router waits for the observable to complete before finalizing the navigation decision. This pattern is essential when your user state is managed in a reactive store.

Using Route Data for Role-Based Access

You can make your guards more generic by reading data properties defined in your route configuration. This allows you to use a single guard for multiple roles.

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

export const roleGuard: CanActivateFn = (route) => {
  const authService = inject(AuthService)
  const router = inject(Router)
  const expectedRole = route.data['role']

  const user = authService.getCurrentUser()

  if (!user || user.role !== expectedRole) {
    return router.parseUrl('/unauthorized')
  }

  return true
}

The ActivatedRouteSnapshot (the route parameter) provides access to the data object. By checking route.data['role'], you can dynamically validate if the current user meets the requirements for that specific route without writing a new guard for every role.

Applying Guards to Route Configurations

Once your functional guards are defined, you must register them in your routing module or your standalone route configuration.

import { Routes } from '@angular/router'
import { authGuard } from './guards/auth.guard'
import { roleGuard } from './guards/role.guard'

export const routes: Routes = [
  {
    path: 'admin',
    loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent),
    canActivate: [authGuard, roleGuard],
    data: { role: 'admin' }
  },
  {
    path: 'profile',
    loadComponent: () => import('./user/profile.component').then(m => m.ProfileComponent),
    canActivate: [authGuard]
  }
]

In this configuration, the admin route is protected by two guards. Angular executes these sequentially. If the first guard returns false or a UrlTree, the subsequent guards are not executed, and the navigation is handled accordingly. This modular approach is exactly how we structure navigation in the Free Angular Admin Template.

Protecting Feature Modules with CanMatch

While canActivate controls if a route can be visited, canMatch is used to decide if a route should even be considered for matching. This is useful for feature-flipping or A/B testing.

import { inject } from '@angular/core'
import { CanMatchFn } from '@angular/router'
import { FeatureService } from './feature.service'

export const featureGuard: CanMatchFn = (route, segments) => {
  const featureService = inject(FeatureService)
  const featureName = route.data?.['feature']

  return featureService.isEnabled(featureName)
}

By using canMatch, if the function returns false, the router will continue searching for other route definitions that might match the URL. This allows you to have two routes with the same path that lead to different components based on application state or permissions.

Best Practice Note:

Always prefer functional guards over class-based guards in modern Angular projects (v15+). They reduce boilerplate and make it easier to share logic. When building complex sidebars or navigation menus, such as those found in the CoreUI Sidebar component, ensure your guards and your menu visibility logic are synchronized to provide a consistent user experience. If you are just starting, you might want to learn how to create a new Angular project to test these security patterns in a fresh environment.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author