Outcome Recipes

Advanced routing patterns for authentication, data preloading, guards, and complex navigation scenarios using @aurelia/router.

These recipes solve complex routing challenges like authentication flows, data preloading, and navigation state management using @aurelia/router. Use them when you need more than basic routing.

1. Global authentication guard for all routes

Goal: Implement centralized authentication checks that run automatically for all routes using the @lifecycleHooks() decorator.

Steps

  1. Create an authentication service to track user state:

    import { observable } from '@aurelia/runtime';
    
    export interface User {
      id: string;
      name: string;
      email: string;
      roles: string[];
    }
    
    export class AuthService {
      @observable isAuthenticated = false;
      currentUser: User | null = null;
    
      constructor() {
        const token = localStorage.getItem('auth_token');
        if (token) {
          this.validateToken(token);
        }
      }
    
      async login(email: string, password: string): Promise<boolean> {
        try {
          const response = await fetch('/api/auth/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ email, password })
          });
    
          if (!response.ok) {
            return false;
          }
    
          const { token, user } = await response.json();
          localStorage.setItem('auth_token', token);
          this.currentUser = user;
          this.isAuthenticated = true;
    
          return true;
        } catch {
          return false;
        }
      }
    
      logout() {
        localStorage.removeItem('auth_token');
        this.currentUser = null;
        this.isAuthenticated = false;
      }
    
      hasRole(role: string): boolean {
        return this.currentUser?.roles.includes(role) ?? false;
      }
    
      private async validateToken(token: string) {
        try {
          const response = await fetch('/api/auth/validate', {
            headers: { 'Authorization': `Bearer ${token}` }
          });
    
          if (response.ok) {
            const user = await response.json();
            this.currentUser = user;
            this.isAuthenticated = true;
          } else {
            localStorage.removeItem('auth_token');
          }
        } catch {
          localStorage.removeItem('auth_token');
        }
      }
    }
  2. Create a global lifecycle hook with @lifecycleHooks() decorator:

    import { lifecycleHooks } from '@aurelia/runtime-html';
    import {
      IRouteViewModel,
      IRouter,
      Params,
      RouteNode,
      NavigationInstruction
    } from '@aurelia/router';
    import { resolve } from '@aurelia/kernel';
    import { AuthService } from './auth-service';
    
    @lifecycleHooks()
    export class GlobalAuthGuard {
      private auth = resolve(AuthService);
      private router = resolve(IRouter);
    
      // List of routes that don't require authentication
      private publicRoutes = ['login', 'register', 'forgot-password', ''];
    
      canLoad(
        viewModel: IRouteViewModel,
        params: Params,
        next: RouteNode,
        current: RouteNode | null
      ): boolean | NavigationInstruction {
        const routePath = next.route.path;
    
        // Check if route is public
        const isPublicRoute = this.publicRoutes.some(path =>
          routePath === path || routePath?.startsWith(`${path}/`)
        );
    
        if (isPublicRoute) {
          return true;
        }
    
        // Check authentication for protected routes
        if (!this.auth.isAuthenticated) {
          // Store intended destination
          sessionStorage.setItem('returnUrl', next.path);
    
          // Redirect to login
          return 'login';
        }
    
        // Check role-based access if route has role requirements
        const requiredRole = next.route.data?.requiredRole;
        if (requiredRole && !this.auth.hasRole(requiredRole)) {
          return 'unauthorized';
        }
    
        return true;
      }
    }
  3. Register the global guard in your app startup:

    import Aurelia from 'aurelia';
    import { RouterConfiguration } from '@aurelia/router';
    import { GlobalAuthGuard } from './global-auth-guard';
    import { AuthService } from './auth-service';
    import { MyApp } from './my-app';
    
    Aurelia
      .register(
        RouterConfiguration,
        AuthService,
        GlobalAuthGuard  // Registered globally - runs for ALL routes
      )
      .app(MyApp)
      .start();
  4. Configure your routes with role metadata:

    import { route } from '@aurelia/router';
    
    @route({
      routes: [
        {
          path: '',
          component: () => import('./pages/home'),
          title: 'Home'
        },
        {
          path: 'login',
          component: () => import('./pages/login'),
          title: 'Login'
        },
        {
          path: 'dashboard',
          component: () => import('./pages/dashboard'),
          title: 'Dashboard'
          // No role needed - just requires authentication
        },
        {
          path: 'admin',
          component: () => import('./pages/admin'),
          title: 'Admin Panel',
          data: { requiredRole: 'admin' }
        }
      ]
    })
    export class MyApp {}
  5. Implement post-login redirect:

    import { IRouter } from '@aurelia/router';
    import { resolve } from '@aurelia/kernel';
    import { AuthService } from './auth-service';
    
    export class Login {
      private router = resolve(IRouter);
      private auth = resolve(AuthService);
    
      email = '';
      password = '';
      error = '';
    
      async submit() {
        const success = await this.auth.login(this.email, this.password);
    
        if (success) {
          // Redirect to intended destination or home
          const returnUrl = sessionStorage.getItem('returnUrl') || 'dashboard';
          sessionStorage.removeItem('returnUrl');
          await this.router.load(returnUrl);
        } else {
          this.error = 'Invalid credentials';
        }
      }
    }

Checklist

  • Global guard runs automatically for ALL routes

  • No need to extend base class or add guard to each component

  • Public routes are whitelisted and bypass auth

  • Protected routes redirect to login when unauthenticated

  • Role-based access works via route metadata

  • Single source of truth for authentication logic

  • Post-login redirect returns user to intended page

When to use this approach

Use global lifecycle hooks when:

  • Most routes require authentication (only a few public routes)

  • You want centralized auth logic in one place

  • You need consistent behavior across the entire app

  • You want to avoid repeating guard code in components

Use component-level hooks when:

  • Only specific routes need protection

  • Each route has unique authorization logic

  • You want fine-grained control per component

  • You need access to component-specific data

2. Data preloading with loading states

Goal: Load required data before showing a route, display loading state during fetch, and handle errors gracefully using the loading lifecycle hook.

Steps

  1. Create a component with data preloading in loading hook:

  2. Show loading state in the template:

  3. Create a reusable preloading base class for consistency:

Checklist

  • Data loads before route is fully activated

  • Loading indicator shows during data fetch

  • Failed data loads prevent navigation and show errors

  • Users can retry failed loads

  • Multiple data sources can be loaded in parallel

  • Throwing errors in loading prevents navigation

3. Preventing navigation with unsaved changes

Goal: Warn users before navigating away from forms with unsaved changes using the canUnload lifecycle hook.

Steps

  1. Create a form component with change tracking:

  2. Bind form inputs to track changes:

  3. Create a reusable mixin for unsaved changes:

Checklist

  • Users are warned before navigating away with unsaved changes

  • Confirmation dialog is shown on navigation attempt

  • Saving clears the unsaved changes flag

  • Navigation is prevented if user cancels

  • Works with browser back button

4. Query parameter state management

Goal: Sync component state with URL query parameters using router state management for shareable, bookmarkable views.

Steps

  1. Create a component that reads and writes query parameters:

  2. Create a template with filter controls:

Checklist

  • Component state syncs to URL query parameters

  • Browser back/forward buttons restore filter state

  • Users can bookmark filtered views

  • Query parameters can be shared via URL

  • Default values are omitted from URL

  • URL updates without full page reload

Router pattern cheat sheet

Pattern
Key Hook
Use When

Global authentication

@lifecycleHooks() + canLoad

Protecting most/all routes from unauthenticated access

Data preloading

loading hook with Promise.all

Data must be ready before showing component

Unsaved changes

canUnload hook with confirmation

Preventing data loss from navigation

Relative nested navigation

router.load(target, { context: resolve(IRouteContext) })

Programmatic sibling/parent navigation from a child component

Restore per-entry UI state

IRouterEvents + window.history.replaceState

Rehydrate filters/scroll positions when users hit Back

Multi-panel dashboards

Named <au-viewport> + router.load('route@viewport')

Keep sidebar, main, and overlay panes in sync

Conditional fallback routing

fallback() returning route IDs

Redirect unknown/disabled paths to supported components

Route-driven menus

resolve(IRouteContext).routeConfigContext.navigationModel

Auto-build nav bars from configured routes

Query parameters

loading hook + router.load

Shareable/bookmarkable filtered views

Best practices

  1. Use replace: true for filter updates: Prevents excessive browser history entries

  2. Preload in parallel: Load multiple data sources with Promise.all in loading hook

  3. Throw errors to prevent navigation: In loading hook, throw to stop navigation on data load failure

  4. Return route name from canLoad: Redirect by returning a string route name instead of calling router.load()

  5. Store return URLs: Use sessionStorage for post-login redirects

  6. Debounce query parameter updates: Prevent excessive URL changes from text input

  7. Use route metadata: Store auth requirements in data property of routes

  8. Handle async properly: All lifecycle hooks can return Promises

See also

Last updated

Was this helpful?