Angular Routing: The Complete Beginner’s Cheat Sheet

Introduction to Angular Routing

Angular Router enables navigation from one view to another as users perform tasks in your single-page applications (SPAs). Instead of loading entirely new pages from the server, the router enables a “virtual page navigation” experience where only the necessary components are loaded and displayed while the rest of the page remains intact. This creates a smoother user experience and reduces load times.

Key Benefits of Angular Routing

  • Single-Page Application Navigation: Navigate between views without full page reloads
  • Lazy Loading: Load feature modules on demand to improve initial load time
  • Route Guards: Protect routes from unauthorized access
  • Route Parameters: Pass data between routes
  • Child Routes: Create nested route hierarchies
  • Route Animations: Add smooth transitions between routes

Setting Up Basic Routing

1. Installing the Router

The Angular Router is included when you create a new project with Angular CLI using the --routing flag:

ng new my-app --routing

For existing projects, ensure @angular/router is in your package.json dependencies.

2. Creating a Routing Module

If you didn’t use the --routing flag, create a routing module manually:

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';

// Define your routes
const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent },
  { path: '**', redirectTo: '' } // Wildcard route for 404
];

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

3. Import Routing Module

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    AboutComponent,
    ContactComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule // Import the routing module
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

4. Add Router Outlet

The <router-outlet> directive tells Angular where to display the component for the currently activated route.

<!-- app.component.html -->
<header>
  <nav>
    <!-- Navigation links go here -->
  </nav>
</header>

<main>
  <router-outlet></router-outlet>
</main>

<footer>
  <!-- Footer content -->
</footer>

Route Configuration Options

Basic Route Properties

PropertyTypeDescription
pathstringURL path segment for this route
componentComponentComponent to display for this route
redirectTostringURL to redirect to
pathMatch‘full’ | ‘prefix’How to match the URL (‘full’ = exact match)
childrenRoutes[]Child routes
loadChildrenFunctionLazy loaded child routes
canActivateCanActivate[]Guards for route activation
canDeactivateCanDeactivate[]Guards preventing leaving a route
dataobjectStatic data for the route
resolveobjectDynamic data resolvers

Route Configuration Examples

const routes: Routes = [
  // Basic route
  { path: 'products', component: ProductListComponent },
  
  // Default route
  { path: '', component: HomeComponent, pathMatch: 'full' },
  
  // Redirect route
  { path: 'old-path', redirectTo: 'new-path', pathMatch: 'full' },
  
  // Route with parameters
  { path: 'product/:id', component: ProductDetailComponent },
  
  // Route with static data
  { path: 'admin', component: AdminComponent, data: { roles: ['ADMIN'] } },
  
  // Route with guard
  { path: 'profile', component: ProfileComponent, canActivate: [AuthGuard] },
  
  // Child routes
  { 
    path: 'user', 
    component: UserComponent,
    children: [
      { path: 'details', component: UserDetailsComponent },
      { path: 'orders', component: UserOrdersComponent }
    ]
  },
  
  // Lazy loaded routes
  { 
    path: 'orders', 
    loadChildren: () => import('./orders/orders.module').then(m => m.OrdersModule) 
  },
  
  // Wildcard route (404)
  { path: '**', component: PageNotFoundComponent }
];

Navigation and Router Links

Template-Based Navigation

Use the routerLink directive to create links:

<!-- Basic link -->
<a routerLink="/home">Home</a>

<!-- Dynamic link -->
<a [routerLink]="['/products', product.id]">{{ product.name }}</a>

<!-- Relative routing (navigate within current route) -->
<a routerLink="../">Go up one level</a>
<a routerLink="./details">Go to details</a>

<!-- With Active Link Highlighting -->
<a routerLink="/products" 
   routerLinkActive="active-link" 
   [routerLinkActiveOptions]="{exact: true}">
  Products
</a>

Programmatic Navigation

Navigate from your component using the Router service:

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

@Component({...})
export class MyComponent {
  constructor(
    private router: Router,
    private route: ActivatedRoute
  ) {}
  
  // Basic navigation
  goToHome() {
    this.router.navigate(['/home']);
  }
  
  // Navigation with parameters
  viewProduct(productId: number) {
    this.router.navigate(['/products', productId]);
  }
  
  // Navigation with relative path
  goToSibling() {
    this.router.navigate(['../sibling'], { relativeTo: this.route });
  }
  
  // Navigation with query parameters
  searchProducts(term: string) {
    this.router.navigate(['/products'], { 
      queryParams: { search: term, category: 'all' } 
    });
  }
  
  // Navigation with fragment (hash)
  goToSection() {
    this.router.navigate(['/page'], { fragment: 'section1' });
  }
  
  // Preserving query parameters
  navigatePreservingParams() {
    this.router.navigate(['/new-route'], {
      queryParamsHandling: 'preserve' // or 'merge'
    });
  }
}

Route Parameters and Data Passing

Route Parameters

Define a route with parameters:

{ path: 'product/:id', component: ProductDetailComponent }

Access parameters in your component:

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

@Component({...})
export class ProductDetailComponent implements OnInit {
  productId: string;
  
  constructor(private route: ActivatedRoute) {}
  
  ngOnInit() {
    // Snapshot approach (doesn't react to changes in the same component)
    this.productId = this.route.snapshot.paramMap.get('id');
    
    // Observable approach (reacts to parameter changes)
    this.route.paramMap.subscribe(params => {
      this.productId = params.get('id');
      // Fetch product details using this.productId
    });
  }
}

Query Parameters

// Setting query parameters in the template
<a [routerLink]="['/products']" [queryParams]="{ category: 'electronics' }">
  Electronics
</a>

// Setting query parameters programmatically
this.router.navigate(['/products'], { 
  queryParams: { category: 'electronics', sort: 'price' } 
});

// Accessing query parameters
ngOnInit() {
  // Snapshot approach
  const category = this.route.snapshot.queryParamMap.get('category');
  
  // Observable approach
  this.route.queryParamMap.subscribe(params => {
    const category = params.get('category');
    const sort = params.get('sort');
    // Filter products based on category and sort
  });
}

Route Data

// Static data in route configuration
const routes: Routes = [
  { 
    path: 'admin', 
    component: AdminComponent, 
    data: { 
      title: 'Admin Panel', 
      permissions: ['manage-users', 'manage-content'] 
    } 
  }
];

// Accessing route data
ngOnInit() {
  // Snapshot approach
  const title = this.route.snapshot.data['title'];
  
  // Observable approach
  this.route.data.subscribe(data => {
    const title = data['title'];
    const permissions = data['permissions'];
    // Use the data
  });
}

Child and Nested Routes

Basic Child Routes

const routes: Routes = [
  { 
    path: 'products', 
    component: ProductsComponent,
    children: [
      { path: '', component: ProductListComponent },
      { path: 'new', component: ProductNewComponent },
      { path: ':id', component: ProductDetailComponent },
      { path: ':id/edit', component: ProductEditComponent }
    ]
  }
];

In the parent component template, add a nested router outlet:

<!-- products.component.html -->
<div class="products-container">
  <h1>Products</h1>
  <nav>
    <a routerLink="/products">All Products</a>
    <a routerLink="/products/new">Add New Product</a>
  </nav>
  
  <!-- Child components will be rendered here -->
  <router-outlet></router-outlet>
</div>

Nested Named Outlets

const routes: Routes = [
  {
    path: 'products',
    component: ProductsComponent,
    children: [
      { path: ':id', component: ProductDetailComponent },
      { 
        path: ':id/edit', 
        component: ProductEditComponent,
        children: [
          { path: 'info', component: ProductInfoComponent, outlet: 'details' },
          { path: 'specs', component: ProductSpecsComponent, outlet: 'details' }
        ]
      }
    ]
  }
];

In the template:

<!-- product-edit.component.html -->
<div class="product-edit">
  <nav>
    <a [routerLink]="['./', { outlets: { details: ['info'] } }]">Basic Info</a>
    <a [routerLink]="['./', { outlets: { details: ['specs'] } }]">Specifications</a>
  </nav>
  
  <!-- Main content -->
  <div class="edit-form">
    <!-- Edit form fields -->
  </div>
  
  <!-- Named outlet -->
  <div class="details-pane">
    <router-outlet name="details"></router-outlet>
  </div>
</div>

Lazy Loading Feature Modules

Setting up a Feature Module for Lazy Loading

  1. Create a feature module with its own routing:
// products/products-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { ProductsComponent } from './products.component';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';

const routes: Routes = [
  {
    path: '', // Empty path since the parent path is defined in app-routing
    component: ProductsComponent,
    children: [
      { path: '', component: ProductListComponent },
      { path: ':id', component: ProductDetailComponent }
    ]
  }
];

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

// products/products.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductsRoutingModule } from './products-routing.module';

import { ProductsComponent } from './products.component';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';

@NgModule({
  declarations: [
    ProductsComponent,
    ProductListComponent,
    ProductDetailComponent
  ],
  imports: [
    CommonModule,
    ProductsRoutingModule
  ]
})
export class ProductsModule { }
  1. In your app routing module, use the loadChildren property:
// app-routing.module.ts
const routes: Routes = [
  { path: '', component: HomeComponent },
  { 
    path: 'products', 
    loadChildren: () => import('./products/products.module').then(m => m.ProductsModule) 
  },
  { 
    path: 'admin', 
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) 
  }
];

Preloading Strategies

// app-routing.module.ts
import { PreloadAllModules } from '@angular/router';

@NgModule({
  imports: [
    // Preload all lazy loaded modules after the app loads
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Creating a custom preloading strategy:

// custom-preloading.strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable()
export class CustomPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // Only preload routes with data.preload set to true
    return route.data && route.data['preload'] ? load() : of(null);
  }
}

// app-routing.module.ts
const routes: Routes = [
  { 
    path: 'frequently-used', 
    loadChildren: () => import('./frequently-used/frequently-used.module').then(m => m.FrequentlyUsedModule),
    data: { preload: true }
  },
  { 
    path: 'rarely-used', 
    loadChildren: () => import('./rarely-used/rarely-used.module').then(m => m.RarelyUsedModule)
    // No preload data, won't be preloaded
  }
];

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

Route Guards

Route guards protect routes from unauthorized access or prevent users from leaving a route with unsaved changes.

Types of Route Guards

Guard TypeInterfacePurpose
CanActivateCanActivateControls if a route can be activated
CanActivateChildCanActivateChildControls if children routes can be activated
CanDeactivateCanDeactivate<T>Controls if a user can leave a route
ResolveResolve<T>Pre-fetches data before route activation
CanLoadCanLoadControls if a lazy loaded module can be loaded

Authentication Guard Example

// auth.guard.ts
import { Injectable } from '@angular/core';
import { 
  CanActivate, 
  ActivatedRouteSnapshot, 
  RouterStateSnapshot, 
  Router 
} from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}
  
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean {
    if (this.authService.isLoggedIn()) {
      return true; // Allow access
    }
    
    // Redirect to login page with return URL
    this.router.navigate(['/login'], { 
      queryParams: { returnUrl: state.url } 
    });
    return false; // Block access
  }
}

Form Deactivation Guard Example

// can-deactivate.guard.ts
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs';

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

@Injectable({
  providedIn: 'root'
})
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(
    component: CanComponentDeactivate
  ): Observable<boolean> | Promise<boolean> | boolean {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

// In your component:
@Component({...})
export class EditProductComponent implements CanComponentDeactivate {
  form: FormGroup;
  originalData: any;
  
  // Implementation of the canDeactivate method
  canDeactivate(): boolean {
    if (this.form.dirty && !this.form.pristine && !this.submitted) {
      return confirm('You have unsaved changes. Do you really want to leave?');
    }
    return true;
  }
}

Using Guards in Route Configuration

const routes: Routes = [
  { 
    path: 'admin', 
    component: AdminComponent, 
    canActivate: [AuthGuard],
    canActivateChild: [AuthGuard],
    children: [
      { path: 'users', component: AdminUsersComponent },
      { path: 'reports', component: AdminReportsComponent }
    ]
  },
  {
    path: 'product/:id/edit',
    component: EditProductComponent,
    canDeactivate: [CanDeactivateGuard],
    resolve: {
      product: ProductResolver
    }
  },
  {
    path: 'secret-module',
    loadChildren: () => import('./secret/secret.module').then(m => m.SecretModule),
    canLoad: [AuthGuard]
  }
];

Resolvers

Resolvers pre-fetch data before activating a route, ensuring the component has the data it needs.

// product.resolver.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { Product } from './product.model';
import { ProductService } from './product.service';

@Injectable({
  providedIn: 'root'
})
export class ProductResolver implements Resolve<Product> {
  constructor(private productService: ProductService) {}
  
  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<Product> {
    const id = route.paramMap.get('id');
    
    return this.productService.getProduct(id).pipe(
      catchError(error => {
        console.error('Error retrieving product:', error);
        return of(null); // Return default data or null
      })
    );
  }
}

// Using the resolver in routes
const routes: Routes = [
  {
    path: 'product/:id',
    component: ProductDetailComponent,
    resolve: {
      product: ProductResolver
    }
  }
];

// Accessing resolved data in the component
ngOnInit() {
  this.route.data.subscribe(data => {
    this.product = data['product'];
  });
}

Route Animations

Add smooth transitions between routes:

// app.component.ts
import { Component } from '@angular/core';
import { 
  RouterOutlet, 
  RouterModule 
} from '@angular/router';
import { 
  trigger, 
  transition, 
  style, 
  animate, 
  query, 
  group 
} from '@angular/animations';

@Component({
  selector: 'app-root',
  template: `
    <div class="page">
      <router-outlet #outlet="outlet"></router-outlet>
    </div>
  `,
  animations: [
    trigger('routeAnimations', [
      transition('* <=> *', [
        // Initial state of new page
        query(':enter', [
          style({ opacity: 0, position: 'absolute' })
        ], { optional: true }),
        
        // Move the old page out
        query(':leave', [
          style({ opacity: 1, position: 'absolute' }),
          animate('300ms ease-out', style({ opacity: 0 }))
        ], { optional: true }),
        
        // Move the new page in
        query(':enter', [
          animate('300ms ease-in', style({ opacity: 1 }))
        ], { optional: true })
      ])
    ])
  ]
})
export class AppComponent {
  prepareRoute(outlet: RouterOutlet) {
    return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
  }
}

// Add animation data to routes
const routes: Routes = [
  { path: 'home', component: HomeComponent, data: { animation: 'HomePage' } },
  { path: 'about', component: AboutComponent, data: { animation: 'AboutPage' } }
];

Common Challenges and Solutions

1. Preserving Route State (scroll position, form values)

Using RouteReuseStrategy to preserve component state:

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

export class CustomRouteReuseStrategy implements RouteReuseStrategy {
  private storedRoutes = new Map<string, DetachedRouteHandle>();
  
  // Whether the route should be stored
  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return route.routeConfig.path === 'products'; // Store only products route
  }
  
  // Store the detached route
  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    if (route.routeConfig.path) {
      this.storedRoutes.set(route.routeConfig.path, handle);
    }
  }
  
  // Whether the route should be retrieved
  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    return !!route.routeConfig && !!this.storedRoutes.get(route.routeConfig.path);
  }
  
  // Retrieve stored route
  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    return this.storedRoutes.get(route.routeConfig.path);
  }
  
  // Whether the same route should be reused
  shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig;
  }
}

// Register in your app module
@NgModule({
  providers: [
    { provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }
  ]
})

2. Handling Permissions Based on User Roles

// role.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root'
})
export class RoleGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}
  
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean {
    // Get the required roles from the route data
    const requiredRoles = route.data['roles'] as Array<string>;
    
    // Check if the user has the required role
    if (this.authService.hasRole(requiredRoles)) {
      return true;
    }
    
    // Redirect to unauthorized page
    this.router.navigate(['/unauthorized']);
    return false;
  }
}

// Using in route configuration
const routes: Routes = [
  { 
    path: 'admin', 
    component: AdminComponent, 
    canActivate: [AuthGuard, RoleGuard],
    data: { roles: ['ADMIN', 'SUPER_ADMIN'] }
  }
];

3. Managing Browser History and Browser Back Button

// Preventing navigation with window:beforeunload
@HostListener('window:beforeunload', ['$event'])
unloadNotification($event: any): void {
  if (this.form.dirty) {
    $event.returnValue = true; // Browser will show confirmation dialog
  }
}

// Disabling browser back button
import { Location } from '@angular/common';

constructor(private location: Location) {
  // Override the back button behavior
  this.location.onPopState(() => {
    // Custom handling here
    return false;
  });
}

Best Practices for Angular Routing

  1. Organize Routes Hierarchically

    • Use feature modules with their own routing
    • Utilize child routes for related components
  2. Use Lazy Loading for Large Applications

    • Each feature module should be lazy loaded
    • Consider preloading strategies for better UX
  3. Always Implement Guards for Protected Routes

    • Use CanActivate for checking permissions
    • Use CanDeactivate for forms with unsaved changes
  4. Handle Navigation Errors

    • Create a 404 Not Found component
    • Use wildcard routes to catch invalid URLs
  5. Use Resolvers for Data Prefetching

    • Prevent rendering components with missing data
    • Show loading indicators during resolution
  6. Maintain Clean URLs

    • Use semantic route paths
    • Prefer route parameters over query parameters for core resources
  7. Use Route Data and Params Wisely

    • Route parameters for resource identifiers
    • Query parameters for filters, sorting, pagination
    • Route data for static configuration
  8. Implement Proper Navigation UX

    • Provide breadcrumbs for deep hierarchies
    • Highlight active routes
    • Preserve user state when appropriate

Resources for Further Learning

Scroll to Top