Munsif.
AboutExperienceProjectsAchievementsBlogsContact
HomeAboutExperienceProjectsAchievementsBlogsContact
Munsif.

Frontend Developer crafting scalable web applications with modern technologies and clean code practices.

Quick Links

  • About
  • Experience
  • Projects
  • Achievements

Connect

© 2025 Shaik Munsif. All rights reserved.

Built with Next.js & Tailwind

Back to Blogs
Angular

Angular Routing v19+: Complete Guide from Basics to Advanced

Master Angular Routing v19+ with functional guards, lazy loading, and modern patterns. From basic navigation to advanced route strategies.

Dec 14, 202422 min read
AngularRoutingNavigationGuardsPerformance

What's New in Angular v19 Routing?

Angular v19 brings significant improvements to routing, building on the momentum of functional guards and standalone components introduced in previous versions. The routing system is now more intuitive, type-safe, and performant.

Key Features in v19+ 🚀

  • Functional Guards: Tree-shakeable, composable route guards
  • Input Binding: Route parameters directly as component inputs
  • View Transitions API: Smooth page transitions
  • Enhanced Type Safety: Better TypeScript inference
  • Improved Preloading: Smarter lazy loading strategies
  • Standalone-First: No NgModules required

Beginner: Getting Started with Angular Routing

Setting Up Routes

The modern way to configure routes uses provideRouter in your application config.

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes)
  ]
};
// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: '**', redirectTo: '' } // Wildcard route
];
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig);

Basic Router Outlet

// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink],
  template: `
    <nav>
      <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a>
      <a routerLink="/about" routerLinkActive="active">About</a>
    </nav>
    <router-outlet />
  `,
  styles: [`
    nav a.active { font-weight: bold; color: #14b8a6; }
  `]
})
export class AppComponent {}

Route Parameters

// app.routes.ts
export const routes: Routes = [
  { path: 'user/:id', component: UserComponent },
  { path: 'product/:id/:slug', component: ProductComponent }
];
// user.component.ts - Traditional way
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-user',
  template: `<h2>User ID: {{ userId }}</h2>`
})
export class UserComponent implements OnInit {
  userId: string | null = null;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    // Snapshot - for one-time read
    this.userId = this.route.snapshot.paramMap.get('id');
    
    // Observable - for reactive updates
    this.route.paramMap.subscribe(params => {
      this.userId = params.get('id');
    });
  }
}

Modern Approach: Input Binding (v16+)

// app.routes.ts
export const routes: Routes = [
  { 
    path: 'user/:id', 
    component: UserComponent,
    // ✅ Enable input binding
  }
];

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withComponentInputBinding() // Enable route params as inputs
    )
  ]
};
// user.component.ts - Modern way with signals
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-user',
  standalone: true,
  template: `
    <h2>User ID: {{ id() }}</h2>
    <p>This updates automatically when route changes!</p>
  `
})
export class UserComponent {
  // ✅ Route parameter directly as input signal
  id = input.required<string>();
  
  // Works with optional params too
  tab = input<string>(); // from query params
}

Programmatic Navigation

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

@Component({
  selector: 'app-navigation-demo',
  template: `
    <button (click)="goToUser(123)">Go to User 123</button>
    <button (click)="goBack()">Go Back</button>
    <button (click)="goToSettings()">Settings with Query</button>
  `
})
export class NavigationDemoComponent {
  private router = inject(Router);

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

  goBack() {
    // Navigate back
    this.router.navigate(['..'], { relativeTo: this.route });
  }

  goToSettings() {
    // Navigation with query params and fragment
    this.router.navigate(['/settings'], {
      queryParams: { tab: 'profile', edit: true },
      fragment: 'personal-info'
    });
    // Results in: /settings?tab=profile&edit=true#personal-info
  }
}

Query Parameters and Fragments

// Reading query params with input binding
@Component({
  selector: 'app-search',
  template: `
    <h2>Search Results</h2>
    <p>Query: {{ query() }}</p>
    <p>Page: {{ page() }}</p>
  `
})
export class SearchComponent {
  // ✅ Query params as inputs (v16+)
  query = input<string>(); // from ?query=...
  page = input<number>(); // from ?page=...
}

// Traditional way
import { ActivatedRoute } from '@angular/router';

@Component({ /*...*/ })
export class TraditionalSearchComponent {
  constructor(private route: ActivatedRoute) {
    this.route.queryParams.subscribe(params => {
      console.log(params['query'], params['page']);
    });
  }
}

Intermediate: Advanced Routing Patterns

Child Routes and Nested Routing

// app.routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    children: [
      { path: '', redirectTo: 'overview', pathMatch: 'full' },
      { path: 'overview', component: OverviewComponent },
      { path: 'stats', component: StatsComponent },
      { path: 'settings', component: SettingsComponent }
    ]
  }
];
// dashboard.component.ts
@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [RouterOutlet, RouterLink],
  template: `
    <div class="dashboard">
      <aside>
        <nav>
          <a routerLink="overview" routerLinkActive="active">Overview</a>
          <a routerLink="stats" routerLinkActive="active">Stats</a>
          <a routerLink="settings" routerLinkActive="active">Settings</a>
        </nav>
      </aside>
      <main>
        <!-- Child routes render here -->
        <router-outlet />
      </main>
    </div>
  `
})
export class DashboardComponent {}

Lazy Loading Routes

Lazy loading loads feature modules only when needed, reducing initial bundle size.

// app.routes.ts
export const routes: Routes = [
  {
    path: 'admin',
    // ✅ Lazy load entire route configuration
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES)
  },
  {
    path: 'blog/:id',
    // ✅ Lazy load single component
    loadComponent: () => import('./blog/blog-post.component')
      .then(m => m.BlogPostComponent)
  }
];
// admin/admin.routes.ts
import { Routes } from '@angular/router';
import { AdminComponent } from './admin.component';
import { UsersComponent } from './users/users.component';
import { RolesComponent } from './roles/roles.component';

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    component: AdminComponent,
    children: [
      { path: 'users', component: UsersComponent },
      { path: 'roles', component: RolesComponent }
    ]
  }
];

Functional Route Guards (Modern Approach)

Functional guards are the modern, recommended way to protect routes.

// guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const authGuard = () => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isLoggedIn()) {
    return true;
  }

  // Redirect to login
  return router.createUrlTree(['/login']);
};

// Role-based guard with parameters
export const roleGuard = (allowedRoles: string[]) => {
  return () => {
    const authService = inject(AuthService);
    const router = inject(Router);
    const userRole = authService.getUserRole();

    if (allowedRoles.includes(userRole)) {
      return true;
    }

    return router.createUrlTree(['/unauthorized']);
  };
};
// app.routes.ts
import { authGuard, roleGuard } from './guards/auth.guard';

export const routes: Routes = [
  {
    path: 'profile',
    component: ProfileComponent,
    canActivate: [authGuard] // ✅ Functional guard
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes'),
    canActivate: [authGuard, roleGuard(['admin', 'superadmin'])]
  },
  {
    path: 'edit/:id',
    component: EditComponent,
    canDeactivate: [unsavedChangesGuard] // Prevent leaving with unsaved changes
  }
];

CanDeactivate Guard - Unsaved Changes

// guards/unsaved-changes.guard.ts
import { CanDeactivateFn } from '@angular/router';

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

export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = 
  (component) => {
    return component.canDeactivate ? 
      component.canDeactivate() : 
      true;
  };
// edit.component.ts
import { Component } from '@angular/core';
import { CanComponentDeactivate } from './guards/unsaved-changes.guard';

@Component({
  selector: 'app-edit',
  template: `
    <form>
      <input [(ngModel)]="formData" />
      <button (click)="save()">Save</button>
    </form>
  `
})
export class EditComponent implements CanComponentDeactivate {
  formData = '';
  private saved = false;

  save() {
    this.saved = true;
    // Save logic
  }

  canDeactivate(): boolean {
    if (!this.saved && this.formData) {
      return confirm('You have unsaved changes. Do you want to leave?');
    }
    return true;
  }
}

Route Resolvers - Pre-fetching Data

// resolvers/user.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { UserService } from '../services/user.service';
import { User } from '../models/user.model';

export const userResolver: ResolveFn<User> = (route, state) => {
  const userService = inject(UserService);
  const userId = route.paramMap.get('id')!;
  
  return userService.getUser(userId);
};
// app.routes.ts
export const routes: Routes = [
  {
    path: 'user/:id',
    component: UserComponent,
    resolve: {
      user: userResolver // Data available before component loads
    }
  }
];
// user.component.ts
import { Component, input } from '@angular/core';
import { User } from '../models/user.model';

@Component({
  selector: 'app-user',
  template: `
    <div *ngIf="user() as userData">
      <h2>{{ userData.name }}</h2>
      <p>{{ userData.email }}</p>
    </div>
  `
})
export class UserComponent {
  // ✅ Resolved data as input
  user = input.required<User>();
}

Route Data and Metadata

// app.routes.ts
export const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent,
    data: { 
      title: 'Home Page',
      breadcrumb: 'Home',
      requiresAuth: false
    }
  },
  {
    path: 'admin',
    component: AdminComponent,
    data: { 
      title: 'Admin Panel',
      roles: ['admin'],
      animation: 'AdminPage'
    }
  }
];
// Reading route data
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({ /*...*/ })
export class PageComponent implements OnInit {
  pageTitle = '';

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    this.route.data.subscribe(data => {
      this.pageTitle = data['title'];
      document.title = this.pageTitle;
    });
  }
}

Advanced: Expert-Level Routing

Custom Route Reuse Strategy

Control when components are reused or recreated during navigation.

// strategies/custom-reuse-strategy.ts
import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';

export class CustomReuseStrategy implements RouteReuseStrategy {
  private handlers: Map<string, DetachedRouteHandle> = new Map();

  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    // Store component if route data says so
    return route.data['shouldReuse'] === true;
  }

  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    if (route.data['shouldReuse']) {
      this.handlers.set(this.getRouteKey(route), handle);
    }
  }

  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    return !!route.data['shouldReuse'] && !!this.handlers.get(this.getRouteKey(route));
  }

  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
    if (!route.data['shouldReuse']) return null;
    return this.handlers.get(this.getRouteKey(route)) || null;
  }

  shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig;
  }

  private getRouteKey(route: ActivatedRouteSnapshot): string {
    return route.pathFromRoot
      .map(r => r.url.map(segment => segment.toString()).join('/'))
      .join('/');
  }
}
// app.config.ts
import { RouteReuseStrategy } from '@angular/router';
import { CustomReuseStrategy } from './strategies/custom-reuse-strategy';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    { provide: RouteReuseStrategy, useClass: CustomReuseStrategy }
  ]
};

// Usage in routes
export const routes: Routes = [
  {
    path: 'search',
    component: SearchComponent,
    data: { shouldReuse: true } // Component will be reused
  }
];

Preloading Strategies

// strategies/custom-preload-strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of, timer } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class CustomPreloadStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // Don't preload if data says so
    if (route.data?.['preload'] === false) {
      return of(null);
    }

    // Immediate preload for high priority
    if (route.data?.['preload'] === 'instant') {
      console.log('Preloading instantly:', route.path);
      return load();
    }

    // Delayed preload for normal priority
    if (route.data?.['preload'] === true) {
      const delay = route.data?.['preloadDelay'] || 2000;
      console.log(`Preloading after ${delay}ms:`, route.path);
      return timer(delay).pipe(mergeMap(() => load()));
    }

    return of(null);
  }
}
// app.config.ts
import { provideRouter, withPreloading } from '@angular/router';
import { CustomPreloadStrategy } from './strategies/custom-preload-strategy';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withPreloading(CustomPreloadStrategy)
    )
  ]
};

// Usage in routes
export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes'),
    data: { preload: true, preloadDelay: 5000 }
  },
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.routes'),
    data: { preload: 'instant' } // Load immediately
  },
  {
    path: 'reports',
    loadChildren: () => import('./reports/reports.routes'),
    data: { preload: false } // Never preload
  }
];

View Transitions API (v19+)

Smooth animations between route changes using the native View Transitions API.

// app.config.ts
import { provideRouter, withViewTransitions } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withViewTransitions() // ✅ Enable view transitions
    )
  ]
};
/* styles.css - Define transitions */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 300ms;
}

/* Slide animation */
@keyframes slide-from-right {
  from { transform: translateX(100%); }
}

@keyframes slide-to-left {
  to { transform: translateX(-100%); }
}

::view-transition-old(root) {
  animation: slide-to-left 300ms ease-out;
}

::view-transition-new(root) {
  animation: slide-from-right 300ms ease-out;
}
// Custom transition per route
import { provideRouter, withViewTransitions } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withViewTransitions({
        skipInitialTransition: true,
        onViewTransitionCreated: ({ transition, from, to }) => {
          console.log('Transitioning from', from, 'to', to);
        }
      })
    )
  ]
};

Router Events and Monitoring

// services/router-monitor.service.ts
import { Injectable, inject } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationError } from '@angular/router';
import { filter } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class RouterMonitorService {
  private router = inject(Router);

  constructor() {
    this.monitorNavigation();
  }

  private monitorNavigation() {
    // Track navigation start
    this.router.events.pipe(
      filter(event => event instanceof NavigationStart)
    ).subscribe((event: NavigationStart) => {
      console.log('Navigation started:', event.url);
      // Show loading indicator
    });

    // Track navigation end
    this.router.events.pipe(
      filter(event => event instanceof NavigationEnd)
    ).subscribe((event: NavigationEnd) => {
      console.log('Navigation ended:', event.url);
      // Hide loading indicator
      // Track analytics
      this.trackPageView(event.urlAfterRedirects);
    });

    // Track navigation errors
    this.router.events.pipe(
      filter(event => event instanceof NavigationError)
    ).subscribe((event: NavigationError) => {
      console.error('Navigation error:', event.error);
      // Handle error
    });
  }

  private trackPageView(url: string) {
    // Send to analytics
    console.log('Page view:', url);
  }
}

Dynamic Route Configuration

// services/dynamic-routes.service.ts
import { Injectable, inject } from '@angular/core';
import { Router, Routes } from '@angular/router';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class DynamicRoutesService {
  private router = inject(Router);
  private http = inject(HttpClient);

  async loadDynamicRoutes() {
    // Fetch route configuration from API
    const dynamicRoutes = await this.http.get<any[]>('/api/routes').toPromise();
    
    const routes: Routes = dynamicRoutes.map(route => ({
      path: route.path,
      loadComponent: () => this.loadComponent(route.component),
      data: route.data
    }));

    // Add routes dynamically
    this.router.resetConfig([
      ...this.router.config,
      ...routes
    ]);
  }

  private loadComponent(componentName: string) {
    // Dynamic component loading
    return import(`./features/${componentName}.component`)
      .then(m => m[`${componentName}Component`]);
  }
}

Auxiliary Routes (Named Outlets)

// app.routes.ts
export const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
    children: [
      {
        path: 'popup',
        component: PopupComponent,
        outlet: 'popup' // Named outlet
      }
    ]
  }
];
// app.component.ts
@Component({
  selector: 'app-root',
  template: `
    <div class="main-content">
      <router-outlet />
    </div>
    
    <!-- Named outlet for popup -->
    <div class="popup-container">
      <router-outlet name="popup" />
    </div>
  `
})
export class AppComponent {}
// Navigation to named outlet
import { Router } from '@angular/router';

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

  openPopup() {
    this.router.navigate([
      { outlets: { popup: ['popup'] } }
    ]);
  }

  closePopup() {
    this.router.navigate([
      { outlets: { popup: null } }
    ]);
  }
}

Common Pitfalls and Solutions

❌ Pitfall 1: Memory Leaks from Route Subscriptions

// ❌ BAD: Subscription not cleaned up
export class BadComponent implements OnInit {
  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    this.route.params.subscribe(params => {
      // Memory leak if component is reused!
    });
  }
}

// ✅ GOOD: Use takeUntilDestroyed
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

export class GoodComponent {
  private destroyRef = inject(DestroyRef);

  constructor(private route: ActivatedRoute) {
    this.route.params.pipe(
      takeUntilDestroyed(this.destroyRef)
    ).subscribe(params => {
      // Automatically cleaned up
    });
  }
}

// ✅ BEST: Use input binding (no subscription needed)
export class BestComponent {
  id = input.required<string>(); // From route params
}

❌ Pitfall 2: Incorrect Relative Navigation

// ❌ BAD: Navigating without considering current route
export class BadComponent {
  constructor(private router: Router) {}

  goToDetails(id: number) {
    this.router.navigate(['details', id]); // Might not work as expected
  }
}

// ✅ GOOD: Use relative navigation with ActivatedRoute
export class GoodComponent {
  constructor(
    private router: Router,
    private route: ActivatedRoute
  ) {}

  goToDetails(id: number) {
    this.router.navigate(['details', id], { 
      relativeTo: this.route 
    });
  }

  goToParent() {
    this.router.navigate(['..'], { 
      relativeTo: this.route 
    });
  }
}

❌ Pitfall 3: Not Handling Navigation Failures

// ❌ BAD: Ignoring navigation result
export class BadComponent {
  async navigate() {
    this.router.navigate(['/somewhere']);
    // What if navigation fails?
  }
}

// ✅ GOOD: Handle navigation result
export class GoodComponent {
  async navigate() {
    const success = await this.router.navigate(['/somewhere']);
    
    if (!success) {
      console.error('Navigation failed');
      // Handle failure (show message, retry, etc.)
    }
  }
}

❌ Pitfall 4: Overusing Resolvers

// ❌ BAD: Blocking navigation for non-critical data
export const heavyResolver: ResolveFn<any> = () => {
  return inject(HeavyService).loadEverything();
  // User waits for ALL data before seeing page
};

// ✅ GOOD: Load critical data in resolver, rest in component
export const criticalResolver: ResolveFn<User> = () => {
  return inject(UserService).getCurrentUser();
  // Only essential data
};

@Component({ /*...*/ })
export class PageComponent implements OnInit {
  user = input.required<User>(); // From resolver
  additionalData = signal(null);

  ngOnInit() {
    // Load non-critical data after page loads
    this.loadAdditionalData();
  }

  private async loadAdditionalData() {
    const data = await this.service.getAdditionalData();
    this.additionalData.set(data);
  }
}

❌ Pitfall 5: Query Param Handling Issues

// ❌ BAD: Losing query params on navigation
export class BadComponent {
  navigate() {
    this.router.navigate(['/next']); 
    // Query params are lost!
  }
}

// ✅ GOOD: Preserve query params
export class GoodComponent {
  navigate() {
    this.router.navigate(['/next'], {
      queryParamsHandling: 'preserve' // Keep existing params
    });
  }

  navigateAndMerge() {
    this.router.navigate(['/next'], {
      queryParams: { newParam: 'value' },
      queryParamsHandling: 'merge' // Merge with existing
    });
  }
}

Decision Tree: Choosing the Right Routing Pattern

Guard Selection Decision Tree

Lazy Loading Strategy Decision


Performance Optimization

Bundle Size Comparison

StrategyInitial BundleLoad TimeBest For
Eager LoadingLargeSlowSmall apps
Lazy LoadingSmallFastLarge apps
Lazy + PreloadSmallFast + SmoothProduction apps
Lazy + Custom PreloadOptimizedBalancedComplex apps

Route Configuration Best Practices

// ✅ GOOD: Organized route structure
export const routes: Routes = [
  // Public routes
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  
  // Auth routes
  { path: 'login', loadComponent: () => import('./auth/login.component') },
  { path: 'register', loadComponent: () => import('./auth/register.component') },
  
  // Protected routes
  {
    path: 'app',
    canActivate: [authGuard],
    children: [
      { path: 'dashboard', loadComponent: () => import('./dashboard/dashboard.component') },
      { path: 'profile', loadComponent: () => import('./profile/profile.component') }
    ]
  },
  
  // Admin routes (lazy loaded module)
  {
    path: 'admin',
    canActivate: [authGuard, roleGuard(['admin'])],
    loadChildren: () => import('./admin/admin.routes'),
    data: { preload: true, preloadDelay: 3000 }
  },
  
  // Error routes
  { path: '404', component: NotFoundComponent },
  { path: '**', redirectTo: '404' }
];

Real-World Example: E-Commerce App Routing

// app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';
import { cartGuard } from './guards/cart.guard';

export const routes: Routes = [
  // Landing page
  {
    path: '',
    loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent),
    title: 'Shop - Home'
  },
  
  // Product catalog
  {
    path: 'products',
    loadChildren: () => import('./features/products/products.routes').then(m => m.PRODUCT_ROUTES),
    data: { preload: 'instant' }
  },
  
  // Single product with SEO-friendly URL
  {
    path: 'product/:id/:slug',
    loadComponent: () => import('./pages/product-detail/product-detail.component'),
    resolve: {
      product: productResolver
    },
    title: productTitleResolver
  },
  
  // Shopping cart
  {
    path: 'cart',
    loadComponent: () => import('./pages/cart/cart.component'),
    canDeactivate: [unsavedChangesGuard]
  },
  
  // Checkout flow
  {
    path: 'checkout',
    canActivate: [authGuard, cartGuard],
    loadChildren: () => import('./features/checkout/checkout.routes'),
    data: { animation: 'CheckoutPage' }
  },
  
  // User account
  {
    path: 'account',
    canActivate: [authGuard],
    loadChildren: () => import('./features/account/account.routes'),
    data: { preload: true, preloadDelay: 2000 }
  },
  
  // Admin panel
  {
    path: 'admin',
    canActivate: [authGuard, roleGuard(['admin', 'manager'])],
    loadChildren: () => import('./features/admin/admin.routes'),
    data: { preload: false }
  },
  
  // Error pages
  { path: 'unauthorized', loadComponent: () => import('./pages/unauthorized/unauthorized.component') },
  { path: '404', loadComponent: () => import('./pages/not-found/not-found.component') },
  { path: '**', redirectTo: '404' }
];
// features/products/products.routes.ts
import { Routes } from '@angular/router';
import { ProductListComponent } from './product-list/product-list.component';
import { CategoryComponent } from './category/category.component';

export const PRODUCT_ROUTES: Routes = [
  {
    path: '',
    component: ProductListComponent
  },
  {
    path: 'category/:categoryId',
    component: CategoryComponent,
    children: [
      {
        path: 'subcategory/:subId',
        component: CategoryComponent
      }
    ]
  },
  {
    path: 'search',
    component: ProductListComponent,
    // Query params: ?q=search&sort=price&filter=instock
  }
];
// guards/cart.guard.ts
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { CartService } from '../services/cart.service';

export const cartGuard = () => {
  const cartService = inject(CartService);
  const router = inject(Router);

  if (cartService.isEmpty()) {
    alert('Your cart is empty!');
    return router.createUrlTree(['/products']);
  }

  return true;
};
// resolvers/product.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn, Router } from '@angular/router';
import { ProductService } from '../services/product.service';
import { Product } from '../models/product.model';
import { catchError, of } from 'rxjs';

export const productResolver: ResolveFn<Product | null> = (route, state) => {
  const productService = inject(ProductService);
  const router = inject(Router);
  const productId = route.paramMap.get('id')!;

  return productService.getProduct(productId).pipe(
    catchError(error => {
      console.error('Product not found:', error);
      router.navigate(['/404']);
      return of(null);
    })
  );
};

export const productTitleResolver: ResolveFn<string> = (route, state) => {
  const product = route.data['product'] as Product;
  return product ? `${product.name} - Shop` : 'Product - Shop';
};

Testing Routes

// product-detail.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { ProductDetailComponent } from './product-detail.component';
import { of } from 'rxjs';

describe('ProductDetailComponent', () => {
  let component: ProductDetailComponent;
  let fixture: ComponentFixture<ProductDetailComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ProductDetailComponent],
      providers: [
        provideRouter([
          {
            path: 'product/:id',
            component: ProductDetailComponent
          }
        ])
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(ProductDetailComponent);
    component = fixture.componentInstance;
  });

  it('should read route parameter', () => {
    // Set input (from route params)
    fixture.componentRef.setInput('id', '123');
    fixture.detectChanges();
    
    expect(component.id()).toBe('123');
  });
});

Migration Guide: Old to New Routing

Before (Angular 14 and earlier)

// Old: NgModule-based
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [ /*...*/ ];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

After (Angular 15+)

// New: Standalone-based
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes)]
};

Class Guards → Functional Guards

// ❌ OLD: Class-based guard
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService) {}

  canActivate(): boolean {
    return this.authService.isLoggedIn();
  }
}

// ✅ NEW: Functional guard
export const authGuard = () => {
  const authService = inject(AuthService);
  return authService.isLoggedIn();
};

Best Practices Summary

✅ DO:

  1. Use functional guards - More tree-shakeable and composable
  2. Use input binding - input() for route params instead of ActivatedRoute
  3. Enable view transitions - Smooth navigation UX
  4. Implement lazy loading - Reduce initial bundle size
  5. Use preloading strategies - Smart background loading
  6. Handle navigation errors - Check navigation results
  7. Clean up subscriptions - Use takeUntilDestroyed()
  8. Use relative navigation - More maintainable
  9. Implement guards - Protect routes properly
  10. Use resolvers wisely - Only for critical data

❌ DON'T:

  1. Don't forget to unsubscribe - Memory leaks
  2. Don't ignore navigation failures - Handle errors
  3. Don't overuse resolvers - Blocks navigation
  4. Don't mutate route data - Immutable patterns
  5. Don't skip error routes - Always have 404
  6. Don't load everything eagerly - Performance impact
  7. Don't use deprecated APIs - Stay current
  8. Don't forget accessibility - Keyboard navigation

Conclusion

Angular v19+ routing represents a significant evolution in how we build navigation in Angular applications. The shift to functional guards, standalone components, and signal-based inputs creates a more modern, performant, and developer-friendly experience.

Key Takeaways 🎯

  • Modern routing is simpler with functional APIs
  • Performance is better with lazy loading and preloading
  • Type safety is stronger with TypeScript inference
  • Developer experience improved with clearer patterns
  • Future-proof with ongoing Angular evolution

Next Steps

  1. Migrate existing apps to standalone components and functional guards
  2. Implement view transitions for better UX
  3. Optimize lazy loading with custom preload strategies
  4. Master signal inputs for cleaner component code
  5. Stay updated with Angular's routing roadmap

Happy routing! 🚀

Written by

Shaik Munsif

Read more articles