With the latest releases of Ionic 8 and Angular 20, building secure and modular apps has never been easier. Thanks to Angular’s new standalone component architecture and functional route guards, protecting routes is both cleaner and more scalable. In this tutorial, you’ll learn how to implement CanActivateFn and CanLoadFn guards, manage user authentication with signals, and build a fully functional login flow using Ionic UI components. Whether you're building admin-only dashboards or private user areas, this guide gives you a modern approach to routing and security in Ionic + Angular apps.
Create a new Ionic and Angular App
Create a new Ionic with an Angular type.
sudo npm install -g @ionic/cli
ionic start ionic-angular-routeguard blank --type=angular
- Choose Standalone
- You can skip creating a free Ionic account
Next, go to the newly created app folder.
cd ./ionic-angular-routeguard
Run Ionic Angular app.
ionic serve
The browser will automatically open, switch to inspect mode, then choose mobile layout.
Create a Custom Ionic Angular HttpInterceptor
Create a folder under the `app` folder.
mkdir src/app/interceptors
Next, create a file for the custom Angular HttpInterceptor.
touch src/app/interceptors/token.interceptor.ts
Update that file.
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { catchError, map, Observable, throwError } from "rxjs";
import { ToastController } from '@ionic/angular/standalone';
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private router: Router,
public toastController: ToastController) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem('token');
if (token) {
request = request.clone({
setHeaders: {
'Authorization': token
}
});
}
if (!request.headers.has('Content-Type')) {
request = request.clone({
setHeaders: {
'content-type': 'application/json'
}
});
}
request = request.clone({
headers: request.headers.set('Accept', 'application/json')
});
return next.handle(request).pipe(
map((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
console.log('event--->>>', event);
}
return event;
}),
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
if (error.error.success === false) {
this.presentToast('Login failed');
} else {
this.router.navigate(['login']);
}
}
return throwError(error);
}));
}
async presentToast(msg: any) {
const toast = await this.toastController.create({
message: msg,
duration: 2000,
position: 'top'
});
toast.present();
}
}
Add to src/main.ts
:
import { bootstrapApplication } from '@angular/platform-browser';
import { RouteReuseStrategy, provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { IonicRouteStrategy, provideIonicAngular } from '@ionic/angular/standalone';
import { routes } from './app/app.routes';
import { AppComponent } from './app/app.component';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { TokenInterceptor } from './app/interceptors/token.interceptor';
bootstrapApplication(AppComponent, {
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
provideIonicAngular(),
provideRouter(routes, withPreloading(PreloadAllModules)),
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
}
],
});
Now, the HTTP interceptor is ready to intercept any request to the API.
Create Ionic Angular Authentication and Book Services
First, create a folder for those services under `src/app/`.
mkdir src/app/services
Create services or providers for authentication and a book.
ionic g service services/auth
ionic g service services/book
Update src/app/services/auth.service.ts
:
import { HttpClient } from '@angular/common/http';
import { computed, Injectable, signal } from '@angular/core';
import { catchError, Observable, of, tap } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthService {
apiUrl = 'http://localhost:3000/api/';
private _loggedIn = signal(false);
readonly isLoggedIn = computed(() => this._loggedIn());
redirectUrl: string | undefined;
constructor(private http: HttpClient) { }
login(data: any): Observable<any> {
return this.http.post<any>(this.apiUrl + 'signin', data)
.pipe(
tap(_ => this._loggedIn.set(true)),
catchError(this.handleError('login', []))
);
}
logout(): Observable<any> {
return this.http.get<any>(this.apiUrl + 'signout')
.pipe(
tap(_ => this._loggedIn.set(false)),
catchError(this.handleError('logout', []))
);
}
register(data: any): Observable<any> {
return this.http.post<any>(this.apiUrl + 'signup', data)
.pipe(
tap(_ => this.log('login')),
catchError(this.handleError('login', []))
);
}
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(error); // log to console instead
this.log(`${operation} failed: ${error.message}`);
return of(result as T);
};
}
private log(message: string) {
console.log(message);
}
}
Create src/app/book.ts
:
export class Book {
_id: number | undefined;
isbn: string | undefined;
title: string | undefined;
author: number | undefined;
publisher: Date | undefined;
__v: number | undefined;
}
Update src/app/services/book.service:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { catchError, Observable, of, tap } from 'rxjs';
import { Book } from '../book';
@Injectable({
providedIn: 'root'
})
export class BookService {
apiUrl = 'http://localhost:3000/api/';
constructor(private http: HttpClient) { }
getBooks(): Observable<Book[]> {
return this.http.get<Book[]>(this.apiUrl + 'book')
.pipe(
tap(_ => this.log('fetched books')),
catchError(this.handleError('getBooks', []))
);
}
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
// TODO: better job of transforming error for user consumption
this.log(`${operation} failed: ${error.message}`);
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
/** Log a HeroService message with the MessageService */
private log(message: string) {
console.log(message);
}
}
Create an Ionic and Angular Page for Displaying Books
Type this command to generate an Ionic and Angular page for the book.
ionic g page book
Update src/app/book/book.page.ts
:
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonContent, IonHeader, IonTitle, IonToolbar, IonIcon, IonButtons, IonList, IonButton, IonItem, IonLabel } from '@ionic/angular/standalone';
import { BookService } from '../services/book.service';
import { AuthService } from '../services/auth.service';
import { Router } from '@angular/router';
import { Book } from '../book';
@Component({
selector: 'app-book',
templateUrl: './book.page.html',
styleUrls: ['./book.page.scss'],
standalone: true,
imports: [IonLabel, IonItem, CommonModule, IonList, IonIcon, IonContent, IonHeader, IonTitle, IonToolbar, FormsModule, IonButtons, IonButton]
})
export class BookPage implements OnInit {
books: Book[] | undefined;
constructor(private bookService: BookService,
private authService: AuthService,
private router: Router) { }
ngOnInit() {
this.getBooks();
}
getBooks(): void {
this.bookService.getBooks()
.subscribe(books => {
console.log(books);
this.books = books;
});
}
logout() {
this.authService.logout()
.subscribe(res => {
console.log(res);
localStorage.removeItem('token');
this.router.navigate(['home']);
});
}
}
Update src/app/book/book.page.html
:
<ion-header>
<ion-toolbar>
<ion-title>List of Books</ion-title>
<ion-buttons slot="end">
<ion-button color="secondary" (click)="logout()">
<ion-icon slot="icon-only" name="exit"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content padding>
<ion-list>
<ion-item *ngFor="let b of books">
<ion-label>{{b.title}}</ion-label>
</ion-item>
</ion-list>
</ion-content>
The book page just shows the list of the book and a log-out button on the title bar.
Create the Ionic and Angular Pages for Login and Register
Create folder src/app/auth
and file.
mkdir src/app/auth
ionic g page auth/login
ionic g page auth/register
Update src/app/auth/login/login.page.ts
:
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, FormsModule, NgForm, Validators, ReactiveFormsModule } from '@angular/forms';
import { IonContent, IonHeader, IonTitle, IonToolbar, ToastController, IonItem, IonCard, IonCardHeader, IonCardContent, IonLabel, IonButton, IonCardTitle } from '@ionic/angular/standalone';
import { AuthService } from 'src/app/services/auth.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-login',
templateUrl: './login.page.html',
styleUrls: ['./login.page.scss'],
standalone: true,
imports: [FormsModule, ReactiveFormsModule, IonCardTitle, IonButton, IonLabel, IonCardContent, IonCardHeader, IonCard, IonItem, IonContent, IonHeader, IonTitle, IonToolbar, CommonModule, FormsModule]
})
export class LoginPage {
loginForm: FormGroup;
constructor(private formBuilder: FormBuilder,
private router: Router,
private authService: AuthService,
public toastController: ToastController) {
this.loginForm = this.formBuilder.group({
'username': [null, Validators.required],
'password': [null, Validators.required]
});
}
onFormSubmit() {
this.authService.login(this.loginForm.value)
.subscribe({
next: (res) => {
if (res.token) {
localStorage.setItem('token', res.token);
this.router.navigate(['book']);
}
}, error: (err) => {
console.log(err);
}
});
}
register() {
this.router.navigate(['register']);
}
async presentToast(msg: any) {
const toast = await this.toastController.create({
message: msg,
duration: 2000,
position: 'top'
});
toast.present();
}
}
Update src/app/auth/login/login.page.html:
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>login</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<form [formGroup]="loginForm" (ngSubmit)="onFormSubmit()">
<ion-card>
<ion-card-header>
<ion-card-title>Please, Sign In</ion-card-title>
</ion-card-header>
<ion-card-content>
<ion-item>
<ion-label position="floating">Username</ion-label>
<ion-input type="text" formControlName="username"></ion-input>
</ion-item>
<ion-item>
<ion-label position="floating">Password</ion-label>
<ion-input type="password" formControlName="password"></ion-input>
</ion-item>
<ion-item lines="none" lines="full" padding-top>
<ion-button
expand="full"
size="default"
shape="round"
color="success"
type="submit"
[disabled]="!loginForm!.valid"
>Login</ion-button
>
</ion-item>
<ion-item lines="none">
<ion-button
type="button"
expand="full"
size="default"
shape="round"
color="medium"
(click)="register()"
>Register</ion-button
>
</ion-item>
</ion-card-content>
</ion-card>
</form>
</ion-content>
Update src/app/auth/register/register.page.ts:
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { IonContent, IonHeader, IonTitle, IonToolbar, ToastController, AlertController, IonCardHeader, IonItem, IonCard, IonCardTitle, IonCardContent, IonLabel, IonButton } from '@ionic/angular/standalone';
import { Router } from '@angular/router';
import { AuthService } from 'src/app/services/auth.service';
@Component({
selector: 'app-register',
templateUrl: './register.page.html',
styleUrls: ['./register.page.scss'],
standalone: true,
imports: [IonButton, IonLabel, IonCardContent, IonCardTitle, IonCard, IonItem, IonCardHeader, FormsModule, ReactiveFormsModule, IonContent, IonHeader, IonTitle, IonToolbar, CommonModule]
})
export class RegisterPage {
registerForm: FormGroup;
constructor(private formBuilder: FormBuilder,
private router: Router,
private authService: AuthService,
public toastController: ToastController,
public alertController: AlertController) {
this.registerForm = this.formBuilder.group({
'username': [null, Validators.required],
'password': [null, Validators.required]
});
}
onFormSubmit() {
this.authService.register(this.registerForm.value)
.subscribe({
next: (res) => {
this.presentAlert('Register Successfully', 'Please login with your new username and password');
}, error: (err) => {
console.log(err);
}
});
}
async presentToast(msg: any) {
const toast = await this.toastController.create({
message: msg,
duration: 2000,
position: 'top'
});
toast.present();
}
async presentAlert(header: string, message: string) {
const alert = await this.alertController.create({
header: header,
message: message,
buttons: [{
text: 'OK',
handler: () => {
this.router.navigate(['login']);
}
}]
});
await alert.present();
}
}
Update src/app/auth/register/register.module.html:
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>register</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<form [formGroup]="registerForm" (ngSubmit)="onFormSubmit()">
<ion-card>
<ion-card-header>
<ion-card-title>Register here</ion-card-title>
</ion-card-header>
<ion-card-content>
<ion-item>
<ion-label position="floating">Username</ion-label>
<ion-input type="text" formControlName="username"></ion-input>
</ion-item>
<ion-item>
<ion-label position="floating">Password</ion-label>
<ion-input type="password" formControlName="password"></ion-input>
</ion-item>
<ion-item lines="none" padding-top>
<ion-button
expand="full"
size="default"
shape="round"
color="success"
type="submit"
[disabled]="!registerForm.valid"
>Register</ion-button
>
</ion-item>
</ion-card-content>
</ion-card>
</form>
</ion-content>
Secure the Guarded Book Page using Angular Route Guard
This is the main part of the Ionic tutorial. Type this command to generate a guard configuration file.
ng generate guard auth/auth
Update that file.
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
import { inject } from '@angular/core';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn() || router.createUrlTree(['/login']);
};
Update src/app/app.routes.ts:
import { Routes } from '@angular/router';
import { authGuard } from './auth/auth.guard';
export const routes: Routes = [
{
path: 'home',
loadComponent: () => import('./home/home.page').then((m) => m.HomePage),
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full',
},
{
path: 'book',
canActivate: [authGuard],
loadComponent: () => import('./book/book.page').then(m => m.BookPage)
},
{
path: 'login',
loadComponent: () => import('./auth/login/login.page').then(m => m.LoginPage)
},
{
path: 'register',
loadComponent: () => import('./auth/register/register.page').then(m => m.RegisterPage)
},
];
Run and Test the Ionic and Angular Securing Pages using Route Guard
Before running the test, the Ionic and Angular secure application, open a new terminal tab to run the MongoDB database.
mongod
In the other tab, run the Node.js and Express.js secure REST API.
nodemon
In the main terminal tab, run the Ionic and Angular application.
ionic serve
So, the whole application will look like this.
Conclusion
In this tutorial, you've learned how to implement secure route-based navigation in an Ionic 8 and Angular 20 standalone application using modern best practices. By leveraging Angular’s functional guards (CanActivateFn
, CanLoadFn
) and reactive signal()
state, you've built a lightweight authentication system that cleanly protects admin-only or restricted pages.
You’ve also seen how to:
-
Use standalone components and lazy-loaded route modules
-
Apply route guards for both navigation and module preloading
-
Build a simple, scalable login/logout flow with Ionic UI
-
Maintain reactive authentication state with Angular’s signals API
This setup provides a clean foundation for building more complex authentication systems, integrating with JWTs, OAuth2, or Firebase Auth as needed. It's fully compatible with Angular's latest standalone and functional programming paradigms—making your app more future-proof and maintainable.
Now you're ready to build fast, modern, and secure Ionic applications with the power of Angular 20. 🚀
You can find the full source code on our GitHub.
We know that building beautifully designed Ionic apps from scratch can be frustrating and very time-consuming. Check Ionic 6 - Full Starter App and save development and design time. Android, iOS, and PWA, 100+ Screens and Components, the most complete and advanced Ionic Template.
That's just the basics. If you need more deep learning about Ionic, Angular, and TypeScript, you can take the following cheap course:
- Ionic Apps with Firebase
- Ionic Apps for WooCommerce: Build an eCommerce Mobile App
- Ionic 8+: Build Food Delivery App from Beginner to Advanced
- IONIC - Build Android & Web Apps with Ionic
- Full Stack Development with Go, Vuejs, Ionic & MongoDB
- Create a Full Ionic App with Material Design - Full Stack
Thanks!