/dashboard in the browser URL and land straight on it — no login needed. That's a problem.This is something almost every Angular app has to deal with. and the fix is Route Guards. They sit between the user and the route, check a condition, and either let the navigation happen or block it and redirect somewhere else.
Not complicated once you see how it works. Let me walk through it.
This tutorial shows how to:
- Understand what Route Guards are and how Angular uses them
- Create an Auth Guard using CanActivate
- Build a simple AuthService to track login status
- Protect specific routes in the app routing module
- Redirect unauthenticated users to the login page
- Use CanActivateChild to protect child routes
Why Route Guards
Let me be clear about one thing first — Route Guards protect navigation inside the Angular app. They don't protect your API. Someone can still call your backend directly even if you block the route in Angular. Your API needs its own authentication, usually JWT tokens or session-based auth.
What Route Guards do is stop the user from seeing pages they shouldn't see in the UI. No guard means anyone can navigate to /admin just by typing it in the address bar. With a guard, Angular checks your condition first and redirects them to login if they're not authenticated.
That's the job. Nothing more, nothing less.
Step 1 : Create an AuthService
Before building the guard, you need something that knows whether the user is logged in or not. That's the AuthService.
Run this command to generate it :
ng generate service auth
Open the generated auth.service.ts and update it :
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private isLoggedIn = false;
constructor(private router: Router) {}
login(username: string, password: string): boolean {
// Replace this with your real API call
if (username === 'admin' && password === '1234') {
this.isLoggedIn = true;
localStorage.setItem('token', 'sample-token');
return true;
}
return false;
}
logout(): void {
this.isLoggedIn = false;
localStorage.removeItem('token');
this.router.navigate(['/login']);
}
isAuthenticated(): boolean {
return localStorage.getItem('token') !== null;
}
}
The isAuthenticated() method checks localStorage for a token. In a real app you'd also validate the token expiry, check with the server, decode a JWT — whatever your auth flow needs. For now this is enough to make the guard work.
Step 2 : Create the Auth Guard
In Angular 14 and above, guards can be written as simple functions instead of classes. It's much cleaner. Here's the functional way :
ng generate guard auth
When the CLI asks which interfaces to implement, select CanActivate.
Open auth.guard.ts and update it :
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.isAuthenticated()) {
return true;
}
// Not logged in — redirect to login page
router.navigate(['/login']);
return false;
};
That's the whole guard. It calls isAuthenticated() from AuthService. If the user is logged in, it returns true and Angular allows the navigation. If not, it redirects to /login and returns false.
inject() is the new way to get dependencies inside functional guards. No constructor needed.
Step 3 : Apply the Guard to Routes
Open app-routing.module.ts and add the guard to the routes you want to protect :
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { authGuard } from './auth.guard';
import { DashboardComponent } from './dashboard/dashboard.component';
import { ProfileComponent } from './profile/profile.component';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard]
},
{
path: 'profile',
component: ProfileComponent,
canActivate: [authGuard]
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
canActivate takes an array so you can apply multiple guards if needed. The guard runs before Angular loads the component. If it returns false, the component never loads.
Home and Login are open routes — no guard. Dashboard and Profile are protected. Simple.
Step 4 : Build the Login Component
The guard redirects to /login when the user isn't authenticated. So that page needs to actually exist and work.
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../auth.service';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-login',
standalone: true,
imports: [FormsModule, CommonModule],
template: `
<div style="max-width:400px; margin:100px auto; padding:20px;">
<h2>Login</h2>
<input [(ngModel)]="username" placeholder="Username" style="width:100%; margin-bottom:10px; padding:8px;" />
<input [(ngModel)]="password" type="password" placeholder="Password" style="width:100%; margin-bottom:10px; padding:8px;" />
<button (click)="login()" style="width:100%; padding:10px;">Login</button>
<p *ngIf="error" style="color:red; margin-top:10px;">{{ error }}</p>
</div>
`
})
export class LoginComponent {
username = '';
password = '';
error = '';
constructor(private authService: AuthService, private router: Router) {}
login() {
const success = this.authService.login(this.username, this.password);
if (success) {
this.router.navigate(['/dashboard']);
} else {
this.error = 'Invalid username or password';
}
}
}
User fills in credentials, clicks login, AuthService validates them, and if successful navigates to dashboard. If not, shows an error message.
In a real app the login() method would call an API, get back a JWT token, store it, then navigate. The structure stays the same.
Step 5 : Protect Child Routes with CanActivateChild
Sometimes your routes have children. Like an admin section with multiple sub-pages. You don't want to add canActivate to every single child. Use canActivateChild on the parent instead and it covers all children automatically.
Update your guard to also export a CanActivateChild function :
import { inject } from '@angular/core';
import { CanActivateFn, CanActivateChildFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
const checkAuth = (): boolean => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
router.navigate(['/login']);
return false;
};
export const authGuard: CanActivateFn = () => checkAuth();
export const authChildGuard: CanActivateChildFn = () => checkAuth();
Then in routing :
{
path: 'admin',
canActivateChild: [authChildGuard],
children: [
{ path: 'users', component: UsersComponent },
{ path: 'settings', component: SettingsComponent },
{ path: 'reports', component: ReportsComponent }
]
}
Now every child of /admin is protected. User has to be authenticated to access /admin/users, /admin/settings, /admin/reports — any of them. Add more children later and they're automatically protected too.
Step 6 : Redirect Back After Login
Right now when a user tries to access /dashboard without being logged in, they get redirected to /login. After logging in, they go to /dashboard — hardcoded. That's not great UX. If they were trying to reach /profile, they should go to /profile after login, not always dashboard.
Update the guard to pass the attempted URL as a query param :
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
};
Then in LoginComponent, read that param and redirect there after successful login :
import { ActivatedRoute } from '@angular/router';
export class LoginComponent {
returnUrl = '/dashboard';
constructor(
private authService: AuthService,
private router: Router,
private route: ActivatedRoute
) {
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard';
}
login() {
const success = this.authService.login(this.username, this.password);
if (success) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.error = 'Invalid username or password';
}
}
}
Now if someone tries to go to /profile, gets redirected to /login?returnUrl=/profile, logs in successfully — they land on /profile. Much better experience.
Common Issues
Guard runs but page still loads
Make sure canActivate is actually attached to the right route in app-routing.module.ts. Easy to add it to the wrong route or misspell the guard name.
Infinite redirect loop
If your /login route also has the guard on it, the redirect will loop forever. Login page should always be an open route with no guard.
Token exists but user gets redirected anyway
Check your isAuthenticated() logic. If you're checking localStorage but storing the token under a different key, it returns null. Console.log the token value inside isAuthenticated() to confirm what's actually there.
Guard works in dev but not after build
Usually a routing issue where routes aren't matching properly after the build. Check your app-routing module for wildcard routes and make sure protected routes are defined before the ** catch-all.
Summary
You learned how to protect Angular routes using Route Guards. You covered :
- What Route Guards do and what they don't do (API security is separate)
- Building an AuthService with isAuthenticated() to track login status
- Creating a functional CanActivate guard using the inject() function
- Applying the guard to specific routes with canActivate in the routing module
- Building a Login component that validates credentials and navigates on success
- Using canActivateChild to protect all children of a parent route at once
- Passing returnUrl as a query param so users land on the right page after login
Route Guards are something every Angular app needs as soon as it has any kind of login. Once you've set it up once it's easy to reuse — just add canActivate to any route and it's protected.
I hope you like this article...
Happy coding! 🚀
0 Comments