Skip to main content
The Angular 18 Archetype uses the Angular Router with functional guards and standalone components. This guide covers route configuration, protection with guards, and lazy loading strategies.

Routes configuration

Routes are defined in app.routes.ts:
// src/app/app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [];
The archetype starts with an empty routes array. This provides a clean slate for building your application’s routing structure.

Adding routes

Basic route

import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home.component';

export const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
  },
  {
    path: 'about',
    component: AboutComponent,
  },
];

Route with children

export const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardLayoutComponent,
    children: [
      { path: '', component: DashboardHomeComponent },
      { path: 'profile', component: ProfileComponent },
      { path: 'settings', component: SettingsComponent },
    ],
  },
];

Redirect route

export const routes: Routes = [
  {
    path: '',
    redirectTo: '/home',
    pathMatch: 'full',
  },
  {
    path: 'home',
    component: HomeComponent,
  },
];

Wildcard route (404)

export const routes: Routes = [
  // ... other routes
  {
    path: '**',
    component: NotFoundComponent,
  },
];
Always place wildcard routes last, as routes are matched in order from top to bottom.

Lazy loading

Lazy loading improves initial load time by loading routes on-demand:

Lazy load a component

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./pages/dashboard/dashboard.component')
      .then(m => m.DashboardComponent),
  },
];

Lazy load child routes

export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES),
  },
];
// src/app/admin/admin.routes.ts
import { Routes } from '@angular/router';

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () => import('./admin-dashboard.component')
      .then(m => m.AdminDashboardComponent),
  },
  {
    path: 'users',
    loadComponent: () => import('./users.component')
      .then(m => m.UsersComponent),
  },
];

Preloading strategy

// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withPreloading(PreloadAllModules) // Preload all lazy routes after initial load
    ),
  ]
};

Using the auth guard

The archetype includes an authGuard to protect routes that require authentication:
// src/app/core/providers/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { environment } from '@env/environment';
import { AuthStore } from '@services/state/auth.store';

export const authGuard: CanActivateFn = () => {
  if (environment.securityOpen) return true;
  const authStore = inject(AuthStore);
  if (authStore.isAuthenticated()) return true;
  const router = inject(Router);
  return router.createUrlTree(['/auth', 'login']);
};

Protecting routes

export const routes: Routes = [
  {
    path: 'profile',
    component: ProfileComponent,
    canActivate: [authGuard],
  },
];

How authGuard works

1

Check environment

If environment.securityOpen is true, allow access (useful for development)
2

Check authentication

Read isAuthenticated() signal from AuthStore
3

Allow or redirect

  • If authenticated: return true (allow navigation)
  • If not authenticated: return UrlTree to /auth/login (redirect)

Creating custom guards

Role-based guard

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthStore } from '@services/state/auth.store';

export const adminGuard: CanActivateFn = () => {
  const authStore = inject(AuthStore);
  const router = inject(Router);
  
  if (!authStore.isAuthenticated()) {
    return router.createUrlTree(['/auth', 'login']);
  }
  
  const user = authStore.user();
  if (user.role === 'admin') {
    return true;
  }
  
  return router.createUrlTree(['/unauthorized']);
};

Unsaved changes guard

import { CanDeactivateFn } from '@angular/router';

export interface CanComponentDeactivate {
  canDeactivate: () => boolean;
}

export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = (component) => {
  if (component.canDeactivate()) {
    return true;
  }
  return confirm('You have unsaved changes. Do you really want to leave?');
};
Usage:
export class EditFormComponent implements CanComponentDeactivate {
  hasUnsavedChanges = false;

  canDeactivate(): boolean {
    return !this.hasUnsavedChanges;
  }
}

// In routes
export const routes: Routes = [
  {
    path: 'edit/:id',
    component: EditFormComponent,
    canDeactivate: [unsavedChangesGuard],
  },
];

Route parameters

Path parameters

export const routes: Routes = [
  {
    path: 'user/:id',
    component: UserDetailComponent,
  },
];
Access in component:
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-user-detail',
  template: `<h1>User ID: {{ userId }}</h1>`
})
export class UserDetailComponent {
  route = inject(ActivatedRoute);
  userId = this.route.snapshot.paramMap.get('id');

  // Or reactive approach
  userId$ = this.route.paramMap.pipe(
    map(params => params.get('id'))
  );
}

Query parameters

import { Component, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
  selector: 'app-search',
  template: `<input (input)="search($event)" />`
})
export class SearchComponent {
  route = inject(ActivatedRoute);
  router = inject(Router);

  ngOnInit() {
    // Read query params
    this.route.queryParamMap.subscribe(params => {
      const query = params.get('q');
      console.log('Search query:', query);
    });
  }

  search(event: Event) {
    const query = (event.target as HTMLInputElement).value;
    // Update query params
    this.router.navigate([], {
      relativeTo: this.route,
      queryParams: { q: query },
      queryParamsHandling: 'merge',
    });
  }
}

Programmatic navigation

import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-example',
  template: `
    <button (click)="goToProfile()">Profile</button>
    <button (click)="goToUser(123)">User 123</button>
    <button (click)="goBack()">Back</button>
  `
})
export class ExampleComponent {
  router = inject(Router);

  goToProfile() {
    this.router.navigate(['/profile']);
  }

  goToUser(id: number) {
    this.router.navigate(['/user', id]);
  }

  goToUserWithQuery(id: number) {
    this.router.navigate(['/user', id], {
      queryParams: { tab: 'settings' }
    });
  }

  goBack() {
    window.history.back();
  }
}

Route data and resolvers

Static route data

export const routes: Routes = [
  {
    path: 'about',
    component: AboutComponent,
    data: { title: 'About Us', showHeader: true },
  },
];
Access in component:
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-about',
  template: `<h1>{{ title }}</h1>`
})
export class AboutComponent {
  route = inject(ActivatedRoute);
  title = this.route.snapshot.data['title'];
}

Resolver for pre-fetching data

import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { HttpClient } from '@angular/common/http';

export const userResolver: ResolveFn<User> = (route) => {
  const http = inject(HttpClient);
  const id = route.paramMap.get('id');
  return http.get<User>(`/api/users/${id}`);
};

// In routes
export const routes: Routes = [
  {
    path: 'user/:id',
    component: UserComponent,
    resolve: { user: userResolver },
  },
];

// In component
export class UserComponent {
  route = inject(ActivatedRoute);
  user = this.route.snapshot.data['user'];
}

Best practices

1

Use lazy loading for feature modules

Split large applications into lazy-loaded modules to reduce initial bundle size
2

Apply guards at parent level

Protect multiple child routes by applying guards to the parent route
3

Use resolvers for critical data

Pre-fetch data with resolvers to ensure it’s available when the component initializes
4

Keep routes organized

Group related routes and use separate route files for large feature areas
5

Use typed route parameters

Create interfaces for route data and parameters to maintain type safety

Build docs developers (and LLMs) love