Introduction to Standalone Routing

Welcome to Chapter 3! In the previous chapters, you built the foundation of your Angular application using standalone components. Now, it’s time to make that application truly dynamic and navigable. Imagine a website with only one page – not very useful, right? That’s where routing comes in!

Routing is the backbone of any modern Single Page Application (SPA), allowing users to move between different “pages” or views within your application without reloading the entire browser page. In the world of Angular, the router maps specific URLs to specific components, rendering them dynamically. This chapter will guide you through setting up and mastering routing in your standalone Angular applications. We’ll explore everything from basic navigation to advanced performance techniques like lazy loading, all while maintaining the clarity and efficiency of the standalone architecture.

By the end of this chapter, you’ll be able to:

  • Configure routing for a standalone Angular application.
  • Navigate between different views using declarative and programmatic methods.
  • Extract data from the URL using route parameters.
  • Implement lazy loading to significantly improve your application’s initial load performance.

Ready to make your Angular app truly interactive? Let’s dive in!

Core Concepts of Standalone Routing

At its heart, Angular’s router is a powerful mechanism that listens to URL changes and renders the appropriate component hierarchy. For standalone applications, the setup is streamlined, moving away from the RouterModule.forRoot/forChild pattern that relied on NgModules.

What is the Angular Router?

The Angular Router is a core library that enables navigation among different views in an Angular application. It interprets a browser URL as an instruction to display a particular component.

Here’s how it generally works:

  1. URL Change: The user clicks a link, types a URL, or navigates programmatically.
  2. Router Intercepts: The Angular Router intercepts this change.
  3. Route Matching: It compares the URL against a predefined set of Routes.
  4. Component Rendering: If a match is found, the Router instantiates the associated component(s) and renders them into a special placeholder in your application’s template.

The Router’s Key Players:

  • Routes array: An array of route definitions that tell the router which component to display for a given URL path.
  • RouterOutlet: A directive that acts as a placeholder where Angular dynamically loads components based on the current route. You’ll typically place this in your root AppComponent.
  • routerLink: A directive used in templates to create navigation links, similar to HTML’s <a> tag but handled by the Angular router.
  • Router service: An injectable service that allows you to navigate programmatically, access route information, and more.
  • ActivatedRoute service: An injectable service that holds information about the route associated with the component that is currently activated. This is how you access route parameters.

Standalone App Setup for Routing: provideRouter

In standalone Angular applications (Angular v15+), you configure routing directly in your application’s bootstrap file, main.ts, using the provideRouter function. This eliminates the need for RouterModule imports in AppModule and feature modules, making your application leaner and more modular.

Why provideRouter? This approach aligns perfectly with the standalone philosophy: components and their dependencies (like routing) are self-contained. provideRouter registers the necessary router services and configuration directly into the application’s root injector.

Basic Route Configuration

Routes are defined as an array of JavaScript objects. Each object typically specifies a path (the URL segment) and a component to render.

// src/app/app.routes.ts (a common convention for standalone apps)
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';

export const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent },
  { path: '', redirectTo: '/home', pathMatch: 'full' }, // Redirect to home for the root URL
  { path: '**', component: HomeComponent } // Wildcard route for any unmatched path
];
  • path: The URL segment.
  • component: The standalone component to load when the path matches.
  • redirectTo: Used for redirecting from one path to another.
  • pathMatch: 'full': Crucial for redirects. It tells the router to only redirect if the entire URL path matches the redirectTo path. If it were 'prefix', it would redirect if the path starts with the redirectTo path, which can lead to unintended redirects.
  • path: '**' (Wildcard Route): This special path matches any URL that hasn’t been matched by previous routes. It’s often used for a “Page Not Found” component or, as in our example, to redirect back to home.

Angular provides two main ways to navigate:

  1. Declarative Navigation (routerLink): This is the most common way, using the routerLink directive in your component templates. It automatically handles preventing full page reloads and updates the browser history.

    <!-- In your AppComponent template -->
    <nav>
      <a routerLink="/home">Home</a> |
      <a routerLink="/about">About</a> |
      <a routerLink="/contact">Contact</a>
    </nav>
    
  2. Programmatic Navigation (Router service): For more complex scenarios, like navigating after a form submission or based on application logic, you can inject the Router service and use its navigate() or navigateByUrl() methods.

    // In a component class
    import { Router } from '@angular/router';
    
    constructor(private router: Router) {}
    
    goToAbout() {
      this.router.navigate(['/about']); // Navigates to /about
    }
    

Route Parameters

Often, you’ll need to pass data through the URL, such as an item’s ID for a detail page (/products/123). This is achieved using route parameters.

// In your routes array
{ path: 'product/:id', component: ProductDetailComponent }

The :id segment is a placeholder for a dynamic value. Inside ProductDetailComponent, you can access this value using the ActivatedRoute service.

// src/app/product-detail/product-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CommonModule } from '@angular/common'; // For NgIf, etc.

@Component({
  standalone: true,
  selector: 'app-product-detail',
  template: `
    <h2>Product Detail</h2>
    <p *ngIf="productId">Product ID: {{ productId }}</p>
    <p *ngIf="!productId">No product selected.</p>
  `,
  imports: [CommonModule],
  styles: [`/* component styles */`]
})
export class ProductDetailComponent implements OnInit {
  productId: string | null = null;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    // Accessing parameters using paramMap (recommended, it's an Observable)
    this.route.paramMap.subscribe(params => {
      this.productId = params.get('id'); // 'id' matches the ':id' in the route path
      console.log('Current Product ID:', this.productId);
      // Here you would typically fetch product data based on this ID
    });
  }
}

Using paramMap (an RxJS Observable) is crucial because a component might be reused for different parameters (e.g., navigating from /product/1 to /product/2 without leaving the ProductDetailComponent). If you only use snapshot.paramMap, the component won’t react to the parameter change.

Lazy Loading Routes for Performance

As your application grows, so does its JavaScript bundle size. Loading everything upfront can lead to slow initial page loads, frustrating users. Lazy loading is a powerful optimization technique where the Angular router only loads the necessary JavaScript code for a particular route when that route is actually activated.

Why is this important in production? Imagine an enterprise application with many features (admin panel, user settings, reporting, etc.). If a typical user only ever interacts with a few of these, loading the code for all features on initial load is a waste. Lazy loading ensures that only the code required for the current view is downloaded, drastically reducing the initial bundle size and improving startup time.

For standalone components, lazy loading is achieved using the loadComponent property in your Routes configuration:

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
// ... other eager components

export const routes: Routes = [
  // ... eager routes
  { path: 'home', component: HomeComponent },
  {
    path: 'admin',
    loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)
  },
  {
    path: 'reports',
    loadComponent: () => import('./reports/reports.component').then(m => m.ReportsComponent)
  },
  // ... redirects and wildcard
];
  • loadComponent: Instead of directly referencing the component, you provide a function that returns a Promise. This Promise resolves to the component class.
  • import('./path/to/component').then(m => m.MyComponent): This is a dynamic import expression, a standard JavaScript feature. It tells the bundler (Webpack or esbuild) to create a separate JavaScript chunk for MyComponent and load it only when this route is accessed. The .then(m => m.MyComponent) part ensures we extract the actual component class from the module object returned by the dynamic import.

Here’s a visual representation of how the router processes routes:

flowchart TD A[User Navigates to URL] --> B{Angular Router} B -->|Matches Path: '/home'| C[Load `HomeComponent`] B -->|Matches Path: '/admin'| D[Dynamic Import: `admin.component.ts`] D -->|Loads JS Chunk| E[Render `AdminComponent`] B -->|No Match| F[Wildcard Route: Redirect/Not Found] C --> G[Rendered UI] E --> G

Step-by-Step Implementation

Let’s build a small Angular application with basic and lazy-loaded routing using standalone components.

1. Create a New Standalone Angular Project

If you don’t have one, let’s create a new Angular project. We’ll use Angular CLI version 17.3.x (or newer, as of 2026-02-11) which defaults to standalone components.

# Ensure you have Angular CLI installed or updated
npm install -g @angular/cli@latest

# Create a new project, say 'angular-router-app'
ng new angular-router-app --standalone --routing --skip-tests --style=css
# Choose 'No' for Server-Side Rendering (SSR) for simplicity in this chapter
cd angular-router-app

The --routing flag automatically sets up app.routes.ts and provideRouter in main.ts.

2. Inspect main.ts and app.routes.ts

Open src/main.ts. You should see something like this:

// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

Now open src/app/app.config.ts:

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

import { routes } from './app.routes'; // Import our routes array

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes)] // This sets up our router!
};

This is where the magic happens! provideRouter(routes) registers the Angular router and its services for your application, using the routes array defined in app.routes.ts.

Next, open src/app/app.routes.ts. Initially, it will be empty or have a basic path.

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

export const routes: Routes = []; // We'll fill this in!

3. Create Basic Standalone Components

Let’s create three simple components: HomeComponent, AboutComponent, and ContactComponent.

ng generate component home --standalone
ng generate component about --standalone
ng generate component contact --standalone

Modify their templates slightly to identify them:

  • src/app/home/home.component.html: <p>Welcome to the Home Page!</p>
  • src/app/about/about.component.html: <p>Learn more About Us.</p>
  • src/app/contact/contact.component.html: <p>Contact Us at example@example.com</p>

4. Configure Basic Routes in app.routes.ts

Now, let’s add our routes to src/app/app.routes.ts.

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';

export const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent },
  { path: '', redirectTo: '/home', pathMatch: 'full' }, // Default route
  { path: '**', redirectTo: '/home', pathMatch: 'full' } // Wildcard route
];

Explanation:

  • We import each standalone component we want to route to.
  • Each object in the routes array maps a path to a component.
  • The '' path with redirectTo and pathMatch: 'full' ensures that if the user navigates to the root URL (/), they are redirected to /home. pathMatch: 'full' is crucial here to ensure it only redirects for the exact root path.
  • The '**' path acts as a fallback. If no other route matches, the user is redirected to /home.

Open src/app/app.component.ts. We need to import RouterOutlet and RouterLink to use them in our template.

// src/app/app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; // For NgIf, etc. if needed
import { RouterOutlet, RouterLink } from '@angular/router'; // Import RouterOutlet and RouterLink

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet, RouterLink], // Add them to imports
  template: `
    <header>
      <h1>My Standalone Angular App</h1>
      <nav>
        <a routerLink="/home" routerLinkActive="active" ariaCurrentWhenActive="page">Home</a> |
        <a routerLink="/about" routerLinkActive="active" ariaCurrentWhenActive="page">About</a> |
        <a routerLink="/contact" routerLinkActive="active" ariaCurrentWhenActive="page">Contact</a>
      </nav>
    </header>

    <main>
      <router-outlet></router-outlet> <!-- This is where components will be rendered -->
    </main>

    <footer>
      <p>&copy; 2026 My Standalone App</p>
    </footer>
  `,
  styles: `
    nav a { margin-right: 10px; text-decoration: none; color: blue; }
    nav a.active { font-weight: bold; color: darkblue; }
    header, footer { background-color: #f0f0f0; padding: 10px; text-align: center; }
    main { padding: 20px; }
  `,
})
export class AppComponent {
  title = 'angular-router-app';
}

Explanation:

  • We added RouterOutlet and RouterLink to the imports array of AppComponent because they are standalone directives/components.
  • The <router-outlet></router-outlet> tag is the placeholder. When you navigate to /home, HomeComponent will be rendered here.
  • routerLink="/path" creates navigation links.
  • routerLinkActive="active" adds the CSS class active to the link when its route is active, allowing you to style the current navigation item.
  • ariaCurrentWhenActive="page" is an accessibility best practice that adds aria-current="page" to the active link, helping screen readers.

Now, run your application:

ng serve -o

You should see your application in the browser, and clicking the navigation links will switch between the Home, About, and Contact pages without a full page reload!

6. Implement Route Parameters

Let’s create a ProductDetailComponent that displays an ID from the URL.

ng generate component product-detail --standalone

Modify src/app/product-detail/product-detail.component.ts:

// src/app/product-detail/product-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router'; // Import ActivatedRoute and RouterLink
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-product-detail',
  standalone: true,
  imports: [CommonModule, RouterLink], // Add RouterLink if you want to link back
  template: `
    <h2>Product Detail</h2>
    <p *ngIf="productId">Displaying details for Product ID: <strong>{{ productId }}</strong></p>
    <p *ngIf="!productId">No product ID provided.</p>
    <button routerLink="/products">Back to Products</button>
  `,
  styles: [`
    button { padding: 8px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; border-radius: 4px; }
    button:hover { background-color: #0056b3; }
  `]
})
export class ProductDetailComponent implements OnInit {
  productId: string | null = null;

  constructor(private route: ActivatedRoute) {} // Inject ActivatedRoute

  ngOnInit() {
    this.route.paramMap.subscribe(params => {
      this.productId = params.get('id'); // Get the 'id' parameter from the URL
      console.log('ProductDetailComponent: Fetched ID:', this.productId);
      // In a real app, you'd use a service to fetch product data here
    });
  }
}

Now, add a route for it in src/app/app.routes.ts and a mock “Products” list component.

ng generate component product-list --standalone

Modify src/app/product-list/product-list.component.html:

<!-- src/app/product-list/product-list.component.html -->
<h2>Our Products</h2>
<ul>
  <li><a routerLink="/product/101">Product A (ID: 101)</a></li>
  <li><a routerLink="/product/102">Product B (ID: 102)</a></li>
  <li><a routerLink="/product/103">Product C (ID: 103)</a></li>
</ul>

And src/app/product-list/product-list.component.ts to import RouterLink:

// src/app/product-list/product-list.component.ts
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router'; // Import RouterLink

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [RouterLink], // Add RouterLink to imports
  templateUrl: './product-list.component.html',
  styleUrl: './product-list.component.css'
})
export class ProductListComponent { }

Update src/app/app.routes.ts to include these new routes:

// src/app/app.routes.ts (updated)
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';
import { ProductListComponent } from './product-list/product-list.component'; // Import
import { ProductDetailComponent } from './product-detail/product-detail.component'; // Import

export const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent },
  { path: 'products', component: ProductListComponent }, // Route to product list
  { path: 'product/:id', component: ProductDetailComponent }, // Route with parameter
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: '**', redirectTo: '/home', pathMatch: 'full' }
];

Finally, update AppComponent’s navigation:

<!-- src/app/app.component.ts template (updated navigation) -->
<nav>
  <a routerLink="/home" routerLinkActive="active" ariaCurrentWhenActive="page">Home</a> |
  <a routerLink="/about" routerLinkActive="active" ariaCurrentWhenActive="page">About</a> |
  <a routerLink="/contact" routerLinkActive="active" ariaCurrentWhenActive="page">Contact</a> |
  <a routerLink="/products" routerLinkActive="active" ariaCurrentWhenActive="page">Products</a>
</nav>

Now, navigating to /products will show the list, and clicking on a product link (e.g., /product/101) will display the ProductDetailComponent with the correct ID. Try changing the ID in the URL directly, and you’ll see the component update thanks to paramMap being an observable!

7. Implement Lazy Loading

Let’s imagine our application has an “Admin” section that only a few users access. We’ll lazy load this section.

First, create a new standalone component for the admin page:

ng generate component admin --standalone

Modify src/app/admin/admin.component.html:

<!-- src/app/admin/admin.component.html -->
<h2>Admin Dashboard</h2>
<p>This content is lazy-loaded! Only visible to authorized users.</p>
<p>Here you might manage users, settings, etc.</p>

Now, update src/app/app.routes.ts to lazy load the AdminComponent. Crucially, remove the direct import of AdminComponent from app.routes.ts.

// src/app/app.routes.ts (final version for this section)
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';
// No direct import of AdminComponent here! It will be lazy loaded.

export const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent },
  { path: 'products', component: ProductListComponent },
  { path: 'product/:id', component: ProductDetailComponent },
  {
    path: 'admin',
    loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)
  }, // Lazy loaded route!
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: '**', redirectTo: '/home', pathMatch: 'full' }
];

Explanation:

  • Notice that AdminComponent is NOT imported at the top of app.routes.ts.
  • The loadComponent property uses a dynamic import() statement. This tells Angular’s build system to create a separate JavaScript bundle for admin.component.ts and its dependencies. This bundle will only be downloaded when the user navigates to /admin.

Finally, add an “Admin” link to AppComponent’s navigation:

<!-- src/app/app.component.ts template (updated navigation) -->
<nav>
  <a routerLink="/home" routerLinkActive="active" ariaCurrentWhenActive="page">Home</a> |
  <a routerLink="/about" routerLinkActive="active" ariaCurrentWhenActive="page">About</a> |
  <a routerLink="/contact" routerLinkActive="active" ariaCurrentWhenActive="page">Contact</a> |
  <a routerLink="/products" routerLinkActive="active" ariaCurrentWhenActive="page">Products</a> |
  <a routerLink="/admin" routerLinkActive="active" ariaCurrentWhenActive="page">Admin</a>
</nav>

Now, restart ng serve (if it was running). Open your browser’s developer tools (Network tab).

  • When you first load the app, observe the network requests. You’ll see main.js, polyfills.js, vendor.js, etc. You won’t see admin.js.
  • Click the “Admin” link. Then, observe the network tab. You should see a new JavaScript chunk being downloaded (e.g., src_app_admin_admin_component_ts.js or similar, depending on your Angular version and build configuration). This confirms that your Admin component is being lazy-loaded!

This incremental approach to code loading is a cornerstone of performant Angular applications.

Mini-Challenge: User Profile with Optional Parameters

Challenge: Create a new standalone component called UserProfileComponent.

  1. Add a route /user/:username that displays the username from the URL.
  2. Also, add a route /user (without a username) that displays a message like “Please select a user.”
  3. Modify your AppComponent’s navigation to include a link to /user/alice and another link to just /user.
  4. In UserProfileComponent, use ActivatedRoute to correctly display the username or the “Please select a user” message.

Hint:

  • Define two routes in your app.routes.ts for /user/:username and /user. The more specific route (/user/:username) should come before the general route (/user) in your routes array. The router matches routes in order.
  • In UserProfileComponent, use this.route.paramMap.subscribe() to check for the presence of the username parameter.

What to observe/learn:

  • How route order affects matching.
  • How to handle optional route parameters and display different content based on their presence.
  • Reinforce the use of paramMap for dynamic parameter changes.

Common Pitfalls & Troubleshooting

  1. Forgetting RouterOutlet: If your application loads but no components appear when you navigate, check if you’ve added <router-outlet></router-outlet> to your AppComponent’s template (or the template of any component that should host child routes).
  2. Incorrect pathMatch:
    • redirectTo: '/home', pathMatch: 'prefix' on the root path '' can lead to an infinite redirect loop, as every path starts with ''. Always use pathMatch: 'full' for the root '' path when redirecting.
    • Not specifying pathMatch (which defaults to 'prefix') for other routes can cause unexpected behavior where a partial match leads to the wrong component. Be explicit.
  3. Missing RouterLink or RouterOutlet in imports: Remember, for standalone components, any Angular directive or component you use in the template (like RouterLink, RouterOutlet, NgIf, NgFor) must be explicitly listed in the imports array of the standalone component where it’s used.
  4. Not Subscribing to paramMap (or queryParamMap): If your component loads correctly but doesn’t update when you navigate between routes that use the same component (e.g., /product/1 to /product/2), you’re likely using this.route.snapshot.paramMap. The snapshot only captures the parameters at the time the component was created. Use this.route.paramMap.subscribe() to react to changes in the route parameters while the component is still active. Don’t forget to unsubscribe in ngOnDestroy for long-lived subscriptions, though paramMap is often handled by Angular if the component itself is destroyed and recreated.
  5. Lazy Loading import() path issues: Double-check the path in your loadComponent function. It’s a relative path from the app.routes.ts file. A common mistake is a typo or incorrect relative path, leading to a “Cannot find module” error in the browser console.
  6. Route Order Matters: If you have routes like /user/profile and /user/:id, the router processes them in the order they appear in the routes array. If /user/:id comes first, it will always match /user/profile as :id will greedily capture profile. Always place more specific routes before more general ones.

Summary

Congratulations! You’ve successfully navigated the world of standalone Angular routing. You now understand:

  • The Power of provideRouter: How to configure your application’s routes directly in app.config.ts for a clean, NgModule-free setup.
  • Defining Routes: How to map URL paths to standalone components, including redirects and wildcard routes.
  • Seamless Navigation: Using routerLink for declarative navigation and the Router service for programmatic control.
  • Dynamic Data with Route Parameters: Extracting information from the URL using the ActivatedRoute service and its paramMap observable.
  • Performance with Lazy Loading: Significantly improving initial load times by dynamically loading component code only when needed, using loadComponent.

Mastering routing is fundamental to building any non-trivial Angular application. In the next chapter, we’ll build upon this foundation by exploring Authentication and Authorization, learning how to protect your routes and components using Guards and Resolvers to ensure only authorized users can access specific parts of your application.

References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.