Back to blog
angularintermediate

Angular Routing & Navigation

Set up the Angular Router, define routes, navigate programmatically, read route params and query strings, protect routes with guards, and lazy load feature modules.

LearnixoApril 16, 20265 min read
AngularRoutingNavigationGuardsLazy LoadingIntermediate
Share:𝕏

The Angular Router maps URLs to components. It supports nested routes, lazy loading, route guards, and history-based navigation — everything a real SPA needs.


Basic Setup

When you run ng new my-app --routing, Angular generates app.routes.ts and wires it up in app.config.ts.

src/app/app.routes.ts

TYPESCRIPT
import { Routes } from '@angular/router';
import { HomeComponent }    from './home/home.component';
import { AboutComponent }   from './about/about.component';
import { NotFoundComponent } from './not-found/not-found.component';

export const routes: Routes = [
  { path: '',        component: HomeComponent },
  { path: 'about',  component: AboutComponent },
  { path: '**',     component: NotFoundComponent },   // catch-all — must be last
];

src/app/app.config.ts

TYPESCRIPT
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

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

app.component.html — add the router outlet:

HTML
<nav>
  <a routerLink="/">Home</a>
  <a routerLink="/about">About</a>
</nav>

<router-outlet />

Import RouterOutlet and RouterLink in your component:

TYPESCRIPT
@Component({
  standalone: true,
  imports: [RouterOutlet, RouterLink],
  // ...
})
export class AppComponent {}

RouterLink & RouterLinkActive

HTML
<!-- Static link -->
<a routerLink="/users">Users</a>

<!-- Dynamic link  uses an array -->
<a [routerLink]="['/users', user.id]">{{ user.name }}</a>

<!-- Link with query params -->
<a [routerLink]="['/users']" [queryParams]="{ page: 2, sort: 'name' }">Page 2</a>

<!-- Active class  applies 'active' when route matches -->
<a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>

<!-- Exact match (root path needs this) -->
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>

Route Parameters

Route Params — /users/:id

TYPESCRIPT
// Route definition
{ path: 'users/:id', component: UserDetailComponent }

Reading the param:

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

@Component({ standalone: true, template: `<p>User ID: {{ userId }}</p>` })
export class UserDetailComponent implements OnInit {
  private route = inject(ActivatedRoute);
  userId!: number;

  ngOnInit(): void {
    this.userId = Number(this.route.snapshot.paramMap.get('id'));
  }
}

For params that change without navigation (same component, different ID):

TYPESCRIPT
ngOnInit(): void {
  this.route.paramMap.subscribe(params => {
    this.userId = Number(params.get('id'));
    this.loadUser(this.userId);
  });
}

Query Params — /users?page=2&sort=name

TYPESCRIPT
ngOnInit(): void {
  this.route.queryParamMap.subscribe(params => {
    this.page = Number(params.get('page') ?? 1);
    this.sort = params.get('sort') ?? 'id';
  });
}

Route Data — Static Metadata

TYPESCRIPT
{ path: 'admin', component: AdminComponent, data: { title: 'Admin Panel', role: 'admin' } }
TYPESCRIPT
ngOnInit(): void {
  const title = this.route.snapshot.data['title'];
}

Programmatic Navigation

TYPESCRIPT
import { Router } from '@angular/router';

@Component({ /* ... */ })
export class LoginComponent {
  private router = inject(Router);

  onLogin(): void {
    // after successful login...
    this.router.navigate(['/dashboard']);

    // with params
    this.router.navigate(['/users', userId]);

    // with query params
    this.router.navigate(['/products'], { queryParams: { category: 'electronics' } });

    // relative navigation
    this.router.navigate(['../sibling'], { relativeTo: this.route });
  }
}

Nested Routes (Children)

TYPESCRIPT
export const routes: Routes = [
  {
    path: 'admin',
    component: AdminLayoutComponent,   // layout component with <router-outlet>
    children: [
      { path: '',        redirectTo: 'dashboard', pathMatch: 'full' },
      { path: 'dashboard', component: AdminDashboardComponent },
      { path: 'users',     component: AdminUsersComponent },
      { path: 'settings',  component: AdminSettingsComponent },
    ],
  },
];

AdminLayoutComponent must have its own <router-outlet> for children to render into.


Lazy Loading (Essential for Performance)

Don't bundle all feature code upfront. Lazy load routes so code is only downloaded when needed:

TYPESCRIPT
export const routes: Routes = [
  { path: '',        component: HomeComponent },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes),
  },
  {
    path: 'shop',
    loadChildren: () => import('./shop/shop.routes').then(m => m.shopRoutes),
  },
];

src/app/admin/admin.routes.ts

TYPESCRIPT
import { Routes } from '@angular/router';

export const adminRoutes: Routes = [
  { path: '',          component: AdminDashboardComponent },
  { path: 'users',     component: AdminUsersComponent },
  { path: 'settings',  component: AdminSettingsComponent },
];

Angular creates a separate JS bundle for each lazy route, significantly improving initial load time.

You can also lazy load a single component:

TYPESCRIPT
{
  path: 'heavy-page',
  loadComponent: () => import('./heavy/heavy.component').then(m => m.HeavyComponent),
}

Route Guards

Guards control who can access a route.

CanActivate Guard

TYPESCRIPT
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './services/auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const auth   = inject(AuthService);
  const router = inject(Router);

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

  // Redirect to login, preserve the attempted URL
  router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
  return false;
};

Apply to routes:

TYPESCRIPT
{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },
{ path: 'admin',     loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes), canActivate: [authGuard, adminGuard] },

CanDeactivate Guard (Unsaved Changes)

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

export interface CanDeactivateComponent {
  hasUnsavedChanges(): boolean;
}

export const unsavedChangesGuard: CanDeactivateFn<CanDeactivateComponent> = (component) => {
  if (component.hasUnsavedChanges()) {
    return confirm('You have unsaved changes. Leave anyway?');
  }
  return true;
};
TYPESCRIPT
@Component({ /* ... */ })
export class EditProfileComponent implements CanDeactivateComponent {
  isDirty = false;

  hasUnsavedChanges(): boolean {
    return this.isDirty;
  }
}

Resolve Guard (Pre-fetch Data)

TYPESCRIPT
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { UserService } from './services/user.service';

export const userResolver: ResolveFn<User> = (route) => {
  return inject(UserService).getById(Number(route.paramMap.get('id')));
};
TYPESCRIPT
{ path: 'users/:id', component: UserDetailComponent, resolve: { user: userResolver } }
TYPESCRIPT
ngOnInit(): void {
  const user = this.route.snapshot.data['user'] as User;
}

Router Events

Listen to navigation events for loading indicators, analytics, etc.:

TYPESCRIPT
import { Router, NavigationStart, NavigationEnd } from '@angular/router';

@Component({ /* ... */ })
export class AppComponent {
  isLoading = false;
  private router = inject(Router);

  constructor() {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) this.isLoading = true;
      if (event instanceof NavigationEnd)   this.isLoading = false;
    });
  }
}

Quick Reference

Setup:              provideRouter(routes) in app.config.ts
Template link:      routerLink="/path" or [routerLink]="['/path', id]"
Active class:       routerLinkActive="active"
Route params:       route.snapshot.paramMap.get('id')
Query params:       route.snapshot.queryParamMap.get('page')
Navigate:           router.navigate(['/path', id], { queryParams: {} })
Nested routes:      children: [] in route,  in parent
Lazy routes:        loadChildren: () => import(...).then(m => m.routes)
Lazy component:     loadComponent: () => import(...).then(m => m.Component)
Guards:             canActivate: [myGuard]  (CanActivateFn)
Pre-fetch:          resolve: { key: myResolver }  (ResolveFn)

Enjoyed this article?

Explore the learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.