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
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'); } } }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; } }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();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 {}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
Create a component with data preloading in
loadinghook:Show loading state in the template:
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
loadingprevents navigation
3. Preventing navigation with unsaved changes
Goal: Warn users before navigating away from forms with unsaved changes using the canUnload lifecycle hook.
Steps
Create a form component with change tracking:
Bind form inputs to track changes:
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
Create a component that reads and writes query parameters:
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
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
Use
replace: truefor filter updates: Prevents excessive browser history entriesPreload in parallel: Load multiple data sources with
Promise.allinloadinghookThrow errors to prevent navigation: In
loadinghook, throw to stop navigation on data load failureReturn route name from
canLoad: Redirect by returning a string route name instead of callingrouter.load()Store return URLs: Use sessionStorage for post-login redirects
Debounce query parameter updates: Prevent excessive URL changes from text input
Use route metadata: Store auth requirements in
dataproperty of routesHandle async properly: All lifecycle hooks can return Promises
See also
Router hooks reference - Complete lifecycle hook documentation
Router events - Subscribe to navigation events
Routing lifecycle - Understanding the routing lifecycle
Error handling patterns - Navigation error handling
Testing guide - Testing router hooks
Last updated
Was this helpful?