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:
- URL Change: The user clicks a link, types a URL, or navigates programmatically.
- Router Intercepts: The Angular Router intercepts this change.
- Route Matching: It compares the URL against a predefined set of
Routes. - 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:
Routesarray: 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 rootAppComponent.routerLink: A directive used in templates to create navigation links, similar to HTML’s<a>tag but handled by the Angular router.Routerservice: An injectable service that allows you to navigate programmatically, access route information, and more.ActivatedRouteservice: 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 theredirectTopath. If it were'prefix', it would redirect if the path starts with theredirectTopath, 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.
Navigating Programmatically and Declaratively
Angular provides two main ways to navigate:
Declarative Navigation (
routerLink): This is the most common way, using therouterLinkdirective 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>Programmatic Navigation (
Routerservice): For more complex scenarios, like navigating after a form submission or based on application logic, you can inject theRouterservice and use itsnavigate()ornavigateByUrl()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 thecomponent, 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 forMyComponentand 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:
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
routesarray maps apathto acomponent. - The
''path withredirectToandpathMatch: '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.
5. Add RouterOutlet and routerLink to AppComponent
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>© 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
RouterOutletandRouterLinkto theimportsarray ofAppComponentbecause they are standalone directives/components. - The
<router-outlet></router-outlet>tag is the placeholder. When you navigate to/home,HomeComponentwill be rendered here. routerLink="/path"creates navigation links.routerLinkActive="active"adds the CSS classactiveto the link when its route is active, allowing you to style the current navigation item.ariaCurrentWhenActive="page"is an accessibility best practice that addsaria-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
AdminComponentis NOT imported at the top ofapp.routes.ts. - The
loadComponentproperty uses a dynamicimport()statement. This tells Angular’s build system to create a separate JavaScript bundle foradmin.component.tsand 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 seeadmin.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.jsor 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.
- Add a route
/user/:usernamethat displays theusernamefrom the URL. - Also, add a route
/user(without a username) that displays a message like “Please select a user.” - Modify your
AppComponent’s navigation to include a link to/user/aliceand another link to just/user. - In
UserProfileComponent, useActivatedRouteto correctly display the username or the “Please select a user” message.
Hint:
- Define two routes in your
app.routes.tsfor/user/:usernameand/user. The more specific route (/user/:username) should come before the general route (/user) in yourroutesarray. The router matches routes in order. - In
UserProfileComponent, usethis.route.paramMap.subscribe()to check for the presence of theusernameparameter.
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
paramMapfor dynamic parameter changes.
Common Pitfalls & Troubleshooting
- Forgetting
RouterOutlet: If your application loads but no components appear when you navigate, check if you’ve added<router-outlet></router-outlet>to yourAppComponent’s template (or the template of any component that should host child routes). - Incorrect
pathMatch:redirectTo: '/home', pathMatch: 'prefix'on the root path''can lead to an infinite redirect loop, as every path starts with''. Always usepathMatch: '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.
- Missing
RouterLinkorRouterOutletinimports: Remember, for standalone components, any Angular directive or component you use in the template (likeRouterLink,RouterOutlet,NgIf,NgFor) must be explicitly listed in theimportsarray of the standalone component where it’s used. - Not Subscribing to
paramMap(orqueryParamMap): If your component loads correctly but doesn’t update when you navigate between routes that use the same component (e.g.,/product/1to/product/2), you’re likely usingthis.route.snapshot.paramMap. Thesnapshotonly captures the parameters at the time the component was created. Usethis.route.paramMap.subscribe()to react to changes in the route parameters while the component is still active. Don’t forget to unsubscribe inngOnDestroyfor long-lived subscriptions, thoughparamMapis often handled by Angular if the component itself is destroyed and recreated. - Lazy Loading
import()path issues: Double-check the path in yourloadComponentfunction. It’s a relative path from theapp.routes.tsfile. A common mistake is a typo or incorrect relative path, leading to a “Cannot find module” error in the browser console. - Route Order Matters: If you have routes like
/user/profileand/user/:id, the router processes them in the order they appear in theroutesarray. If/user/:idcomes first, it will always match/user/profileas:idwill greedily captureprofile. 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 inapp.config.tsfor a clean, NgModule-free setup. - Defining
Routes: How to map URL paths to standalone components, including redirects and wildcard routes. - Seamless Navigation: Using
routerLinkfor declarative navigation and theRouterservice for programmatic control. - Dynamic Data with Route Parameters: Extracting information from the URL using the
ActivatedRouteservice and itsparamMapobservable. - 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
- Angular Routing & Navigation Official Guide
- Angular
provideRouterAPI Reference - Angular
ActivatedRouteAPI Reference
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.