Angular Route Guards: Class-Based vs Functional Guards
These three questions are closely related. The major change is:
Older Angular
↓
Class-based Guards
↓
implements CanActivate
Modern Angular
↓
Functional Guards
↓
CanActivateFn + inject()
1. How Were Class-Based Guards Implemented?
What is a Class-Based Guard?
A class-based guard is the traditional Angular approach where we create a class that implements a Router guard interface such as:
CanActivate
CanActivateChild
CanDeactivate
CanMatch
For authentication, the class commonly implemented:
CanActivate
and Angular's Dependency Injection provided services through the constructor.
Full Class-Based Authentication Guard Example
auth.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private loggedIn = false;
login(): void {
this.loggedIn = true;
}
logout(): void {
this.loggedIn = false;
}
isLoggedIn(): boolean {
return this.loggedIn;
}
}
auth.guard.ts
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot
} from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {
}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
if (this.authService.isLoggedIn()) {
return true;
}
this.router.navigate(['/login']);
return false;
}
}
app.routes.ts
import { Routes } from '@angular/router';
import { Dashboard } from './dashboard';
import { Login } from './login';
import { AuthGuard } from './auth.guard';
export const routes: Routes = [
{
path: 'login',
component: Login
},
{
path: 'dashboard',
component: Dashboard,
canActivate: [AuthGuard]
}
];
How Class-Based Guard Works
The class:
export class AuthGuard implements CanActivate
implements:
CanActivate
Therefore, it must provide:
canActivate(...)
Angular executes this method before activating the route.
/dashboard
↓
AuthGuard
↓
canActivate()
↓
isLoggedIn()?
/ \
Yes No
↓ ↓
true /login
↓
Dashboard
Dependencies were normally constructor-injected:
constructor(
private authService: AuthService,
private router: Router
)
And because the guard was a DI-managed class, it commonly had:
@Injectable({
providedIn: 'root'
})
2. What Are Functional Route Guards?
What is a Functional Guard?
A functional route guard is a guard implemented as a function instead of a class.
Modern Angular commonly uses functional guard types such as:
CanActivateFn
CanActivateChildFn
CanDeactivateFn
CanMatchFn
Instead of:
class AuthGuard implements CanActivate
we can write:
export const authGuard: CanActivateFn = () => {
// guard logic
};
Full Functional Guard Example
auth.guard.ts
import { inject } from '@angular/core';
import {
CanActivateFn,
Router
} from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn =
(route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
return router.createUrlTree(['/login']);
};
Route configuration:
{
path: 'dashboard',
component: Dashboard,
canActivate: [authGuard]
}
Notice the difference:
Older
canActivate: [AuthGuard]
AuthGuard is a class.
Modern
canActivate: [authGuard]
authGuard is a function.
How Does Dependency Injection Work?
In a class-based guard, dependencies were commonly injected through the constructor:
constructor(
private authService: AuthService,
private router: Router
)
In a functional guard, we commonly use:
inject()
For example:
const authService = inject(AuthService);
const router = inject(Router);
So:
Class Guard
↓
Constructor Injection
Functional Guard
↓
inject()
Class-Based vs Functional Guard
| Class-Based | Functional |
|---|---|
| Traditional approach | Modern approach |
| Uses a class | Uses a function |
implements CanActivate |
CanActivateFn |
| Commonly uses constructor injection | Uses inject() |
| Requires more boilerplate | Less boilerplate |
AuthGuard |
authGuard |
Class
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService
) {
}
canActivate(): boolean {
return this.authService.isLoggedIn();
}
}
Functional
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
return authService.isLoggedIn();
};
The functional version is much smaller.
3. How Do You Create a Functional Authentication Guard?
Let's build a complete standalone Angular example.
The requirement is:
/dashboard
should only be accessible when the user is logged in.
Otherwise:
/dashboard
↓
Not authenticated
↓
/login
Full Program
auth.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private loggedIn = false;
login(): void {
this.loggedIn = true;
}
logout(): void {
this.loggedIn = false;
}
isLoggedIn(): boolean {
return this.loggedIn;
}
}
auth.guard.ts
import { inject } from '@angular/core';
import {
CanActivateFn,
Router
} from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn =
(route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
return router.createUrlTree(['/login']);
};
login.ts
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
@Component({
selector: 'app-login',
standalone: true,
template: `
<h2>Login</h2>
<button (click)="login()">
Login
</button>
`
})
export class Login {
constructor(
private authService: AuthService,
private router: Router
) {
}
login(): void {
this.authService.login();
this.router.navigate(['/dashboard']);
}
}
dashboard.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-dashboard',
standalone: true,
template: `
<h2>Dashboard</h2>
<p>
Welcome! You are authenticated.
</p>
`
})
export class Dashboard {
}
app.routes.ts
import { Routes } from '@angular/router';
import { Login } from './login';
import { Dashboard } from './dashboard';
import { authGuard } from './auth.guard';
export const routes: Routes = [
{
path: 'login',
component: Login
},
{
path: 'dashboard',
component: Dashboard,
canActivate: [authGuard]
},
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
}
];
app.ts
import { Component } from '@angular/core';
import {
RouterLink,
RouterOutlet
} from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [
RouterLink,
RouterOutlet
],
template: `
<h1>Authentication Guard Demo</h1>
<nav>
<a routerLink="/login">Login</a>
|
<a routerLink="/dashboard">Dashboard</a>
</nav>
<hr>
<router-outlet></router-outlet>
`
})
export class App {
}
app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes)
]
};
main.ts
import { bootstrapApplication }
from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';
bootstrapApplication(App, appConfig)
.catch(err => console.error(err));
How the Functional Authentication Guard Works
The protected route is:
{
path: 'dashboard',
component: Dashboard,
canActivate: [authGuard]
}
When the user requests:
/dashboard
Angular executes:
authGuard
The guard gets the authentication service:
const authService = inject(AuthService);
and Router:
const router = inject(Router);
Then:
if (authService.isLoggedIn()) {
return true;
}
If authenticated:
/dashboard
↓
authGuard
↓
isLoggedIn()
↓
true
↓
Dashboard
If not authenticated:
return router.createUrlTree(['/login']);
Flow:
/dashboard
↓
authGuard
↓
isLoggedIn()
↓
false
↓
/login
Why Return UrlTree Instead of navigate()?
You may see code like:
if (!authService.isLoggedIn()) {
router.navigate(['/login']);
return false;
}
It can work, but a cleaner guard pattern is to return the redirect:
return router.createUrlTree(['/login']);
The guard is then simply telling Angular Router:
Don't activate /dashboard
↓
Redirect to /login
rather than manually starting another navigation from inside the guard.
Functional Guard Types
Modern Angular provides functional equivalents for the major route guards:
| Purpose | Functional Type |
|---|---|
| Protect entering route | CanActivateFn |
| Protect child routes | CanActivateChildFn |
| Protect leaving route | CanDeactivateFn<T> |
| Control route matching | CanMatchFn |
For example:
const guard: CanActivateFn
const guard: CanActivateChildFn
const guard: CanDeactivateFn<MyComponent>
const guard: CanMatchFn
Advantages of Functional Guards
- Less boilerplate.
- No guard class required.
- No
implements CanActivate. - No
@Injectable()needed for the guard function itself. - Works naturally with
inject(). - Easy to read for simple authentication/authorization logic.
- Fits modern standalone Angular patterns well.
Key Points
Older Angular
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService
) {
}
canActivate(): boolean {
return this.authService.isLoggedIn();
}
}
Modern Angular
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
return authService.isLoggedIn();
};
Remember:
OLD
↓
Class
↓
implements CanActivate
↓
Constructor DI
MODERN
↓
Function
↓
CanActivateFn
↓
inject()
Interview Questions and Answers
1. What is a class-based route guard?
A class-based route guard is the traditional Angular approach where a class implements a Router guard interface such as CanActivate.
export class AuthGuard implements CanActivate {
}
Dependencies are commonly provided using constructor injection.
2. What is a functional route guard?
A functional route guard implements route-guard logic using a function rather than a class.
For example:
export const authGuard: CanActivateFn = () => {
return true;
};
3. How do you inject a service into a functional guard?
Use Angular's inject() function:
const authService = inject(AuthService);
4. How do you create a functional authentication guard?
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
return router.createUrlTree(['/login']);
};
Then configure it:
{
path: 'dashboard',
component: Dashboard,
canActivate: [authGuard]
}
5. Class-based vs functional guards?
Class-based guards use classes and guard interfaces such as:
implements CanActivate
Functional guards use function types such as:
CanActivateFn
and commonly use:
inject()
for dependencies.
6. Do functional guards require @Injectable()?
The guard function itself does not require @Injectable().
Its dependencies, such as AuthService, are still provided through Angular's DI system.
7. Can functional guards return an Observable or Promise?
Yes. Functional guards can make synchronous or asynchronous navigation decisions.
Interview-ready answer
Earlier Angular applications commonly implemented route guards as injectable classes that implemented interfaces such as
CanActivate, with dependencies provided through constructor injection. Modern Angular supports functional route guards such asCanActivateFn,CanDeactivateFn, andCanMatchFn. Functional guards are simpler and can access dependencies using theinject()function. For an authentication guard, we typically useCanActivateFnto check the authentication state and returntruewhen authenticated or return a redirect to the login page when unauthenticated.