Skip to main content

Routing & Navigation

Rodando Passenger uses Angular’s standalone routing system with layout-based route organization, lazy loading, and route guards for authentication.

Route Configuration

The main routing configuration is defined in app.routes.ts:
src/app/app.routes.ts:6-136
export const routes: Routes = [
  // ==== Public Zone (auth) → blocked if already logged in ====
  {
    path: 'auth',
    loadComponent: () => import('./features/auth/auth.component'),
    canActivate: [guestGuard],
    canLoad: [guestGuardCanLoad],
    children: [
      {
        path: '',
        loadChildren: () => import('./features/auth/auth.routes'),
      },
    ],
  },

  // ==== Private Zone (tabs + sidebar) → requires session ====
  {
    path: '',
    component: DefaultLayoutComponent, // Header + Content + TabBar + Sidebar
    canActivateChild: [authGuardWithRefreshChild],
    children: [
      // === Tabs ===
      {
        path: 'home',
        loadComponent: () => import('./features/tabs/home/home.component'),
        data: { title: 'Inicio', tab: 'home' },
      },
      {
        path: 'trips',
        loadComponent: () => import('./features/tabs/trips/trips.component'),
        children: [
          { path: '', redirectTo: 'active', pathMatch: 'full' },
          {
            path: 'active',
            loadComponent: () =>
              import('./features/tabs/trips/active/active.component'),
            data: { title: 'Viaje Activo', tab: 'trips' },
          },
          {
            path: 'history',
            loadComponent: () =>
              import('./features/tabs/trips/historial/historial.component'),
            data: { title: 'Historial', tab: 'trips' },
          },
        ],
      },
      {
        path: 'profile',
        loadComponent: () =>
          import('./features/tabs/profile/profile.component'),
        data: { title: 'Perfil', tab: 'profile' },
      },

      // === Sidebar pages (same visual layers) ===
      {
        path: 'wallet',
        loadComponent: () =>
          import('./features/sidebar/wallet/wallet.component'),
        data: { title: 'Wallet' },
      },
      {
        path: 'settings',
        loadComponent: () =>
          import('./features/sidebar/settings/settings.component'),
        data: { title: 'Configuración' },
      },
      // ... more sidebar routes ...

      // Redirects
      { path: 'trips', redirectTo: 'trips/active', pathMatch: 'full' },
      { path: '', redirectTo: 'home', pathMatch: 'full' },
    ],
  },

  // ==== Fullscreen (outside layout) → also private ====
  {
    path: 'map',
    loadComponent: () => import('./features/tabs/map/map.component'),
    canActivate: [authGuardWithRefresh],
  },

  // Wildcard
  { path: '**', redirectTo: '' },
];

Route Structure

The app has three main route zones:
Path: /auth/*Protected by: guestGuard (blocks if authenticated)Routes:
  • /auth/login - Login page
  • /auth/signup - Registration page
Layout: No layout (fullscreen auth pages)
Layout-based routing groups routes by visual layout. Routes under DefaultLayoutComponent automatically get the tab bar and sidebar, while /auth/* and /map render fullscreen.

Route Guards

Authentication Guard (with Auto-Refresh)

Protects private routes and attempts token refresh if expired:
src/app/core/guards/auth.guard.ts:76-86
export const authGuardWithRefresh: CanActivateFn = (route, state) => {
  const authStore = inject(AuthStore);
  const authFacade = inject(AuthFacade);
  const router = inject(Router);

  // Already authenticated? Allow immediately
  try { 
    if (authStore.isAuthenticated && authStore.isAuthenticated()) 
      return true; 
  } catch {}

  // Try silent refresh, redirect to login if fails
  return tryRefreshIfSupported(authStore, authFacade).pipe(
    map(ok => (ok ? true : toLogin(router, state.url)))
  );
};

Token Refresh Logic

The guard attempts to refresh the access token before redirecting:
src/app/core/guards/auth.guard.ts:38-71
function tryRefreshIfSupported(authStore: AuthStore, authFacade: AuthFacade): Observable<boolean> {
  // Already authenticated? No need to refresh
  try {
    if (authStore.isAuthenticated && authStore.isAuthenticated()) 
      return of(true);
  } catch {}

  // Check if facade supports refresh
  const canPerform = typeof (authFacade as any).performRefresh === 'function';
  if (!canPerform) return of(false);

  // Execute performRefresh and normalize result
  const raw = (authFacade as any).performRefresh?.();
  const obs$ = normalizeToObservable(raw).pipe(take(1));

  return obs$.pipe(
    map((tokenOrVal) => {
      // Check if store is now authenticated (side effect)
      try {
        if (authStore.isAuthenticated && authStore.isAuthenticated()) 
          return true;
      } catch {}

      // If result is a token string
      if (typeof tokenOrVal === 'string' && tokenOrVal.length > 0) 
        return true;

      // If result is an object with accessToken
      if (tokenOrVal && typeof tokenOrVal === 'object' && 
          typeof (tokenOrVal as any).accessToken === 'string') 
        return true;

      return false;
    }),
    catchError(() => of(false))
  );
}
Smart Refresh: The guard checks if the user is authenticated, attempts a silent token refresh if needed, and only redirects to login if refresh fails. This provides seamless UX for returning users.

Guest Guard

Prevents authenticated users from accessing auth pages:
src/app/core/guards/auth.guard.ts:123-136
export const guestGuard: CanActivateFn = (route, state) => {
  const authStore = inject(AuthStore);
  const router = inject(Router);

  // Not authenticated? Allow access to auth pages
  try {
    if (!authStore.isAuthenticated || !authStore.isAuthenticated()) 
      return true;
  } catch {
    return true;
  }

  // Already authenticated → redirect to returnUrl or home
  const returnUrl = (route.queryParams && route.queryParams['returnUrl']) ?? '/';
  return router.createUrlTree([returnUrl]);
};

Child Route Guards

Protects all child routes under a parent:
src/app/core/guards/auth.guard.ts:91-94
export const authGuardWithRefreshChild: CanActivateChildFn = (childRoute, state) => {
  // Reuse the same logic as the main guard
  return authGuardWithRefresh(childRoute, state);
};

Lazy Loading

All feature modules are lazy-loaded using dynamic imports:
// ❌ BAD: Eager loading (increases initial bundle size)
import { HomeComponent } from './features/tabs/home/home.component';

// ✅ GOOD: Lazy loading (splits into separate chunk)
loadComponent: () => import('./features/tabs/home/home.component')

Benefits of Lazy Loading

Faster Initial Load

Only loads code needed for the current route

Smaller Bundle Size

Splits app into smaller chunks loaded on-demand

Better Performance

Reduces memory usage and parse time

Code Splitting

Each feature gets its own JavaScript bundle

Lazy Loading Child Routes

Child routes can also be lazy-loaded:
Auth Routes Example
{
  path: 'auth',
  loadComponent: () => import('./features/auth/auth.component'),
  children: [
    {
      path: '',
      loadChildren: () => import('./features/auth/auth.routes'), // ← Lazy-loaded routes
    },
  ],
}
src/app/features/auth/auth.routes.ts
import { Routes } from '@angular/router';

export default [
  {
    path: 'login',
    loadComponent: () => import('./login/login.component'),
  },
  {
    path: 'signup',
    loadComponent: () => import('./signup/signup.component'),
  },
  { path: '', redirectTo: 'login', pathMatch: 'full' },
] as Routes;

Tab Navigation

The app uses Ionic tabs for primary navigation:
Tab Routes (within DefaultLayoutComponent)
{
  path: 'home',
  loadComponent: () => import('./features/tabs/home/home.component'),
  data: { title: 'Inicio', tab: 'home' },
},
{
  path: 'trips',
  loadComponent: () => import('./features/tabs/trips/trips.component'),
  data: { title: 'Viajes', tab: 'trips' },
},
{
  path: 'profile',
  loadComponent: () => import('./features/tabs/profile/profile.component'),
  data: { title: 'Perfil', tab: 'profile' },
}
The data.tab property is used by the layout component to highlight the active tab. Secondary pages (settings, wallet, etc.) use the sidebar:
{
  path: 'wallet',
  loadComponent: () => import('./features/sidebar/wallet/wallet.component'),
  data: { title: 'Wallet' },
},
{
  path: 'settings',
  loadComponent: () => import('./features/sidebar/settings/settings.component'),
  data: { title: 'Configuración' },
}
These routes render within DefaultLayoutComponent but are accessed via the sidebar menu.

Nested Routes

Some features have nested child routes:
Trips Feature with Nested Routes
{
  path: 'trips',
  loadComponent: () => import('./features/tabs/trips/trips.component'),
  children: [
    { path: '', redirectTo: 'active', pathMatch: 'full' },
    {
      path: 'active',
      loadComponent: () => import('./features/tabs/trips/active/active.component'),
      data: { title: 'Viaje Activo', tab: 'trips' },
    },
    {
      path: 'history',
      loadComponent: () => import('./features/tabs/trips/historial/historial.component'),
      data: { title: 'Historial', tab: 'trips' },
    },
  ],
}
  • /trips → redirects to /trips/active
  • /trips/active → shows active trip
  • /trips/history → shows trip history

Programmatic Navigation

Using Router

Components inject Router for navigation:
import { Router } from '@angular/router';

export class ExampleComponent {
  private router = inject(Router);

  navigateToHome() {
    this.router.navigate(['/home']);
  }

  navigateWithParams() {
    this.router.navigate(['/trips', 'active'], {
      queryParams: { id: '123' },
    });
  }
}
In templates, use routerLink directive:
<a routerLink="/home">Home</a>
<a [routerLink]="['/trips', 'active']">Active Trip</a>
<a routerLink="/settings" [queryParams]="{ section: 'account' }">Settings</a>
Facades typically navigate after successful operations:
Example: Navigate after login
login(payload: LoginPayload) {
  this.authFacade.login(payload)
    .pipe(take(1))
    .subscribe({
      next: (user) => {
        this.router.navigate(['/home']);
      },
      error: (err) => {
        // Handle error
      },
    });
}

Route Data and Meta

Route Data

Routes can carry custom data:
{
  path: 'home',
  loadComponent: () => import('./features/tabs/home/home.component'),
  data: { 
    title: 'Inicio', 
    tab: 'home',
    requiresPermission: 'view_dashboard' 
  },
}
Access in component:
import { ActivatedRoute } from '@angular/router';

export class HomeComponent {
  private route = inject(ActivatedRoute);

  ngOnInit() {
    const title = this.route.snapshot.data['title'];
    console.log(title); // 'Inicio'
  }
}

Query Parameters

Handle query params for filtering/state:
// Navigate with query params
this.router.navigate(['/trips/history'], {
  queryParams: { status: 'completed', page: 2 },
});

// Read query params
this.route.queryParams.subscribe(params => {
  const status = params['status'];
  const page = params['page'];
});

Route Guards Summary

Purpose: Protects single routesBehavior:
  • Checks if user is authenticated
  • Attempts silent token refresh if expired
  • Redirects to /auth/login?returnUrl=... if refresh fails
Usage: canActivate: [authGuardWithRefresh]
Purpose: Protects all child routesBehavior:
  • Same as authGuardWithRefresh but applies to all children
  • Used on parent layout routes
Usage: canActivateChild: [authGuardWithRefreshChild]
Purpose: Prevents lazy-loading bundle for unauthorized usersBehavior:
  • Runs before loading the module code
  • More efficient than canActivate for unauthorized users
Usage: canLoad: [authGuardWithRefreshCanLoad]
Purpose: Blocks auth pages for authenticated usersBehavior:
  • Checks if user is NOT authenticated
  • Redirects to home (or returnUrl) if already logged in
Usage: canActivate: [guestGuard] on /auth/* routes

Best Practices

Use Layout Components

Group routes by visual layout for code reuse and consistency

Lazy Load Everything

Use dynamic imports for all feature routes to reduce bundle size

Guard Child Routes

Use canActivateChild on parent to protect all children at once

Handle returnUrl

Save original URL in guards to redirect after login

Next Steps

Layouts

Learn about DefaultLayoutComponent and MapLayoutComponent

State Management

See how guards use AuthStore to check authentication

Build docs developers (and LLMs) love