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 callingnavigate()inside the guard. state.urlis the address the user tried to open, so you can return to it.- Other guard types exist:
canDeactivate(warn about unsaved changes),canMatchandcanActivateChild.
Try it yourself
- Add a
canDeactivateguard that asks for confirmation when leaving a form with unsaved text. - Hide the Dashboard link in the navigation when the user is signed out.
- Write down why a guard alone cannot protect private data.
Quick check
- What can a
CanActivateFnreturn? - Why is
inject()allowed inside a guard? - Where must real data protection happen?
Next, continue with Angular Guards: CanActivateChild, CanDeactivate, Resolve, canMatch and Preloading.