Angular Route Guards: Protect Pages with CanActivateFn

Angular Tutorial: Beginner to Advanced · 9. Routing and navigation · 4 min read · Updated

Some pages should only be seen by signed-in users. A route guard is a small function that runs before a page opens and decides whether to allow it. In this lesson you build a fake login and protect a dashboard.

Important: a guard only improves the user experience. Real security must always be checked on the server, because anyone can change code that runs in the browser.

What you will learn

  • How to write a functional guard with CanActivateFn
  • How to redirect with UrlTree
  • How to send the user back to the page they asked for

The auth service

// src/app/auth/auth.ts
import { Injectable, computed, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class Auth {
  private readonly user = signal<string | null>(null);

  readonly isLoggedIn = computed(() => this.user() !== null);
  readonly userName = this.user.asReadonly();

  login(name: string): void {
    this.user.set(name);
  }

  logout(): void {
    this.user.set(null);
  }
}

The guard

// src/app/auth-guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { Auth } from './auth/auth';

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

  if (auth.isLoggedIn()) {
    return true;
  }
  // Send the visitor to /login and remember where they wanted to go.
  return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};

A guard can return true to allow, false to cancel, or a UrlTree to redirect. Inside a guard you can call inject() because it runs in an injection context.

Login and dashboard pages

// src/app/auth/login.ts
import { Component, inject, input } from '@angular/core';
import { Router } from '@angular/router';
import { Auth } from './auth';

@Component({
  selector: 'app-login',
  template: `
    <h2>Login</h2>
    <p>Demo only: any name works.</p>
    <label for="name">Name</label>
    <input #nameInput id="name" type="text" />
    <button type="button" (click)="signIn(nameInput.value)">Sign in</button>
  `,
})
export class Login {
  private readonly auth = inject(Auth);
  private readonly router = inject(Router);

  readonly returnUrl = input<string>();

  protected signIn(name: string): void {
    this.auth.login(name.trim() || 'Guest');
    this.router.navigateByUrl(this.returnUrl() || '/dashboard');
  }
}
// src/app/auth/dashboard.ts
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { Auth } from './auth';

@Component({
  selector: 'app-dashboard',
  template: `
    <h2>Dashboard</h2>
    <p>Hello, {{ auth.userName() }}. This page is only for signed-in users.</p>
    <button type="button" (click)="signOut()">Sign out</button>
  `,
})
export class Dashboard {
  protected readonly auth = inject(Auth);
  private readonly router = inject(Router);

  protected signOut(): void {
    this.auth.logout();
    this.router.navigateByUrl('/');
  }
}

Add the routes

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { Home } from './pages/home';
import { About } from './pages/about';
import { NotFound } from './pages/not-found';
import { ProductList } from './products/product-list';
import { ProductDetail } from './products/product-detail';
import { Login } from './auth/login';
import { Dashboard } from './auth/dashboard';
import { authGuard } from './auth-guard';

export const routes: Routes = [
  { path: '', component: Home, title: 'Home' },
  { path: 'about', component: About, title: 'About' },
  { path: 'products', component: ProductList, title: 'Products' },
  { path: 'products/:id', component: ProductDetail, title: 'Product details' },
  { path: 'login', component: Login, title: 'Login' },
  { path: 'dashboard', component: Dashboard, canActivate: [authGuard], title: 'Dashboard' },
  { path: '**', component: NotFound, title: 'Not found' },
];
<!-- src/app/app.html -->
<header>
  <nav aria-label="Main">
    <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
    <a routerLink="/products" routerLinkActive="active">Products</a>
    <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
    <a routerLink="/about" routerLinkActive="active">About</a>
  </nav>
</header>
<main>
  <router-outlet />
</main>

Open /dashboard while signed out. You are sent to /login?returnUrl=%2Fdashboard, and after signing in you land on the dashboard.

The key parts

  • canActivate: [authGuard] attaches the guard to a route.
  • Returning router.createUrlTree(...) redirects, which is better than calling navigate() inside the guard.
  • state.url is the address the user tried to open, so you can return to it.
  • Other guard types exist: canDeactivate (warn about unsaved changes), canMatch and canActivateChild.

Try it yourself

  1. Add a canDeactivate guard that asks for confirmation when leaving a form with unsaved text.
  2. Hide the Dashboard link in the navigation when the user is signed out.
  3. Write down why a guard alone cannot protect private data.

Quick check

  1. What can a CanActivateFn return?
  2. Why is inject() allowed inside a guard?
  3. Where must real data protection happen?

Next, continue with Angular Guards: CanActivateChild, CanDeactivate, Resolve, canMatch and Preloading.

Frequently asked questions

Is the old class-based CanActivate still used?

Functional guards are the current recommended style. They are shorter and easier to test.

Can a guard load data?

It can, but use a resolver or load data inside the page. Guards should decide access and stay quick.