Sooner or later, anyone starting with Angular runs into routing. The first encounter with a Router configuration can feel like staring at an array stuffed with cryptic properties: path, children, loadChildren, canActivate, resolve.

What follows is a tour of routing fundamentals in Angular. We'll look at what the Router actually is, how application paths get defined, how those definitions grow into larger structures, and how plain route loading compares with lazy loading. Guards and resolvers get a brief mention too, so you'll recognize them in a configuration and understand what they're there for.

This isn't a complete treatment of the Angular Router. The point is to form a mental picture of how routing works and to be able to read the configuration of an app you didn't write.

For a deeper dive, see Miłosz's article.

Routing

Nearly every web application is made up of several screens: a home page, a product list, a single product's details, a user profile, an admin panel. Each of those views usually maps to its own URL:

/
/products
/products/42
/profile
/admin

In a traditional application, requesting a different address typically pulls a completely new HTML document from the server. Angular, by contrast, is a framework built for Single Page Applications — SPAs. Once the app has loaded for the first time, moving between its views can happen without reloading the whole page.

That's the job of routing.

Routing is how we say which part of the application should appear for a given URL. Angular ships an official mechanism for this: the Angular Router.

Router

In short, the Router is what governs navigation inside the application.

When the user heads for some address, the router consults the routing configuration and searches for a path that matches it. Once it finds one, it can render the component attached to that path.

You'll also hear the word Route used alongside Router. The two are easy to conflate, but they're distinct:

Router – the mechanism that carries out navigation

Route – a single rule that says what should happen when the user reaches a particular address

Routing configuration

The router checks the routing configuration — so what does that configuration look like?

Since standalone components became the norm, you'll typically find it in app.routes.ts. In NgModule-based applications, routing tends to live in its own module, such as app-routing.module.ts.

Either way, paths are declared identically.

import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProductsComponent } from './products/products.component';
import { ProductDetailsComponent } from './product-details/product-details.component';

export const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
  },
  {
    path: 'products',
    component: ProductsComponent,
  },
  {
    path: 'products/:id',
    component: ProductDetailsComponent,
  },
];

Routes is an array holding the definitions of the individual routes. Every object in it stands for one route, or for one fragment of the routing tree.

path – the portion of the address the Router should match against

component – the component Angular should activate once this route matches

So visiting myApp.com/products activates ProductsComponent.

That raises an obvious question: where on the page does this component actually show up?

RouterOutlet

The main template of the application might contain something like this:

<header>...</header>

<router-outlet />

<footer>...</footer>

Think of RouterOutlet as a slot set aside for the Router. Whatever component corresponds to the currently active route gets rendered there.

This is what allows the header and footer to remain in place while the Router swaps out only the content between them.

Keep in mind that RouterOutlet isn't limited to a single location. Nested routes, or so-called named outlets — several outlets at the same level — do occur in practice. That deserves its own article. So if you ever spot multiple router outlets in a codebase, dear Reader, don't be alarmed. It's perfectly normal 🙂

Moving from one path to another, say from /products to /home, can be done with Angular's routerLink directive, placed on an HTML element.

<a routerLink="/">Home Page</a>
<a routerLink="/products">Products</a>
<a routerLink="/profile">Profile</a>

Navigation can also be triggered from a .ts file through the aforementioned Router:

private readonly router = inject(Router);

openProducts(): void {
  this.router.navigate(['/products']);
}

Templates suit the first approach; programmatic navigation comes in handy when reaching another screen is a consequence of application logic.

ActivatedRoute

ActivatedRoute stands for the route that is currently active, and it gives a component access to information tied to the current URL.

Among other things, it exposes path parameters, query parameters and any data assigned to the route.

For the address /products/42, the component can read the id parameter like this:

private readonly activatedRoute = inject(ActivatedRoute);

ngOnInit(): void {
  const id = this.activatedRoute.snapshot.paramMap.get('id');
}

ActivatedRoute is thus the object through which a component learns the routing context in which it was started and what data the current route passed along.

Starting with Angular 16, a URL parameter can also arrive directly as a component input, without injecting ActivatedRoute at all. First enable the feature in the router configuration inside app.config.ts:

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

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

Then, in the component, simply declare an input whose name matches the parameter in the path (products/:id):

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-product-details',
  template: `<p>Product ID: {{ id() }}</p>`,
})
export class ProductDetails {
  readonly id = input.required<string>();
}

Routing as a tree

Early on, it's tempting to read routing as a plain list of addresses:

/home
/products
/profile
/settings

In reality, thinking of routing as a tree pays off much better.

Suppose the application also has an admin panel:

/admin
/admin/users
/admin/settings

Then the structure can be pictured like this:

/
├── products
│   └── :id
├── profile
└── admin
    ├── users
    └── settings

Here, users and settings are routes belonging to the admin section.

Angular expresses that relationship through children, which lets us nest further arrays of routes.

export const routes: Routes = [
  {
    path: 'products',
    component: ProductsComponent,
  },
  {
    path: 'admin',
    component: AdminComponent,
    children: [
      {
        path: 'users',
        component: UsersComponent,
      },
      {
        path: 'settings',
        component: SettingsComponent,
      },
    ],
  },
];

This is precisely where a second router outlet can come into play — inside the admin route.

Of course, the whole routing configuration doesn't have to sit in one file. It can be split across many files placed in different parts of the project.

app.routes.ts

products/
  products.routes.ts

admin/
  admin.routes.ts

profile/
  profile.routes.ts

Routing trees and splitting them into smaller segments lead naturally to lazy loading, a concept that comes up constantly in Angular.

Lazy loading – we don’t have to load everything at application startup

In every example so far, the components used by routing were imported directly and referenced through the component property. That means their code ships as part of what's needed for the application's initial load.

In a small application this rarely hurts. As the project grows, though, more and more independent parts accumulate:

  • the home page,
  • a product catalog,
  • a shopping cart,
  • a user account,
  • an admin panel,
  • a reporting system.

A user may open the application only to browse a few products. Should the code for the admin panel and reports really be delivered the moment the app first opens?

Not necessarily.

That's the situation lazy loading addresses: loading certain parts of the application only at the moment they're needed.

With plain routing, we might write:

import { AboutComponent } from './about/about.component';

export const routes: Routes = [
  {
    path: 'about',
    component: AboutComponent,
  },
];

Here AboutComponent is imported directly and tied to the route.

With lazy loading, loadComponent takes over:

export const routes: Routes = [
  {
    path: 'about',
    loadComponent: () =>
      import('./about/about.component')
        .then(m => m.AboutComponent),
  },
];

Under this configuration, the component's code is downloaded only when the user tries to activate that route. Angular can then divide the application into separate chunks fetched on demand.

The same holds for the children approach:

  • plain routing
export const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
  },
  {
    path: 'admin',
    component: AdminComponent,
    children: [
      {
        path: 'users',
        component: UsersComponent,
      },
      {
        path: 'settings',
        component: SettingsComponent,
      },
    ],
  },
];
  • lazy loading
export const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
  },
  {
    path: 'admin',
    loadChildren: () =>
      import('./admin/admin.routes')
        .then(m => m.ADMIN_ROUTES),
  },
];

In this setup the main configuration knows an /admin branch exists, while the details of its routing are loaded only when required.

That said, not every route has to be lazy loaded. Lazy loading shrinks the amount of JavaScript needed for the initial load, but each later navigation then has to fetch another chunk. Which is why the Angular documentation frames lazy loading as a tool whose use should follow your application's structure.

Guards and resolvers

The routing configuration can carry more than the path and the component. Additional mechanisms can be wired up to run during navigation.

For now, the essential skill is recognizing guards and resolvers inside a configuration. How they're implemented is a separate, more advanced subject.

Guard

A guard decides whether the user may reach a given route — for instance by checking that the user is logged in or holds the right permissions.

{
  path: 'admin',
  component: AdminComponent,
  canActivate: [authGuard],
}

Here canActivate means that before the Router activates AdminComponent, it must run the specified guard.

If the guard permits the navigation, the Router may continue activating the route. If the guard refuses, the user never reaches that view. A guard can also return something that redirects the user elsewhere.

Picture it as a checkpoint:

user wants to go to /admin
            ↓
          guard
         /     \
  allowed?     not allowed?
     ↓
   /admin

Angular offers several kinds of guards for different situations. They let you control, among other things, whether a route can be entered, whether it can be matched, or whether the current screen can be left.

Resolver

A resolver prepares the data a route needs before that route is activated.

Take the case where the user navigates to /products/42.

The routing configuration contains:

{
  path: 'products/:id',
  component: ProductDetailsComponent,
  resolve: {
    product: productResolver,
  },
}

The :id fragment is a route parameter. For the address /products/42, its value is 42.

Before the Router activates ProductDetailsComponent, it runs the productResolver function. That resolver can read the id parameter's value, use it to fetch the data of product 42, and hand the result over to the route.

As a result, the component can start with its data already prepared, rather than kicking off the fetch only after the screen has been entered.

Summary

A routing configuration can begin life looking quite modest:

{
  path: 'products',
  component: ProductsComponent,
}

After learning a few more features, however, you may run into a route resembling this:

{
  path: 'products/:id',
  loadComponent: () =>
    import('./product-details/product-details.component')
      .then(m => m.ProductDetailsComponent),
  canActivate: [authGuard],
  resolve: {
    product: productResolver,
  },
}

And although far more elements are present at first glance, all of them describe a single thing — what should happen when the user navigates to a particular part of the application.

  • The Router needs to find a matching route.
  • It may load the code required for it.
  • It may run guards.
  • It may run resolvers.

Finally, it activates the relevant piece of the routing tree and shows it in the RouterOutlet.

The Angular Router's actual navigation process is more involved, with its own stages and events, but none of that is necessary knowledge at the beginning.

A quick recap of the key concepts:

Router manages navigation.

Routes describes the configuration of available routes.

Route describes a single route or a fragment of the tree.

RouterOutlet marks the place where the activated component can appear.

children lets you build a nested routing tree.

loadComponent and loadChildren let you load parts of the application only when they're needed.

Guards let you influence whether navigation can happen.

Resolvers let you prepare the data a route requires before it's activated.

These are only the basics of the Angular Router, yet they already make it far easier to find your way around the routing configuration of an existing application. The next time you open app.routes.ts, instead of a list of mysterious objects, you can read it as a map of your application's structure and how to move through it.