Secure Ionic 8 and Angular 20 Apps with Route Guards

by Didin J. on Jul 22, 2025 Secure Ionic 8 and Angular 20 Apps with Route Guards

Learn how to protect pages in Ionic 8 and Angular 20 using standalone routing, CanActivateFn, CanLoadFn, and signals for reactive auth with route guards.

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.

Ionic 4 and Angular 7 Tutorial: Securing Pages using Route Guard - Blank Ionic


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:

Thanks!