Modern routing in Angular is functional and lazy by default: routes are plain objects, pages load with loadComponent/loadChildren, and everything that comes from the URL (params, query, data, resolver results) goes straight into the component’s input()s. No ActivatedRoute, no subscribe.
Configuration
export const routes: Routes = [
{ path: '', loadComponent: () => import('./features/home/home-page') }, // a page
{ path: 'products', loadChildren: () => import('./features/products/products.routes') }, // a feature
{ path: 'old-products', redirectTo: 'products' },
{ path: '**', loadComponent: () => import('./core/layout/not-found'), title: 'Page not found' },
];
// app.config.ts
provideRouter(
routes,
withComponentInputBinding(), // params / query / data / resolve → input()
withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }),
withViewTransitions(), // animations between pages (View Transitions API)
// withPreloading(PreloadAllModules),
);How the router picks a route
The router reads the routes from top to bottom and takes the first one that matches the whole URL. A :id segment matches anything, ** matches everything, and redirectTo starts the search again with the new URL. That's why the order of the routes matters.
/books/new → NewBook
- ✗ '' pathMatch 'full', but 'books/new' is left
- ✗ 'books' the start matches, but 'new' is left over
- ✓ 'books/new' matches the whole URL
With books/:id above books/new, the page for a new book can never be reached: “new” becomes an id. Specific paths first, parameters after them, ** last. The two configuration mistakes don't even get that far: the router refuses the routes when the app starts, with the same NG04014 messages you see here.
// specific paths first, parameters after them, the wildcard last
export const routes: Routes = [
{ path: '', redirectTo: 'books', pathMatch: 'full' }, // '' + redirect: always pathMatch: 'full'
{ path: 'books', loadComponent: () => import('./books/book-list') },
{ path: 'books/new', loadComponent: () => import('./books/new-book') }, // before :id, or 'new' becomes an id
{ path: 'books/:id', loadComponent: () => import('./books/book-detail') },
{ path: 'books/:id/edit', loadComponent: () => import('./books/edit-book') },
{ path: 'about', loadComponent: () => import('./about') }, // no leading slash in path
{ path: '**', loadComponent: () => import('./not-found') }, // last: anything nobody else matched
];Parameters as inputs (live)
The demo below uses real child routes of this page: /routing/user/:id. The topic’s route file (routing.routes.ts) is loaded lazily, and the child component receives everything through input().
Pick a user on the left. The URL changes, and the child component receives :id as an input.
@Component({ /* ... */ })
export class UserDetail {
readonly id = input.required({ transform: numberAttribute }); // :id (path param)
readonly tab = input<string>(); // ?tab= (query param)
readonly section = input<string>(); // data.section
readonly area = input<string>(); // data.area, inherited from the PARENT
readonly user = httpResource<UserDto>(() => `/api/users/${this.id()}`);
}Inheriting parameters — change in v22 v22
// v22 (breaking): paramsInheritanceStrategy now defaults to 'always'
// → child routes inherit params, data and resolve from ALL parents.
{
path: 'orgs/:orgId',
data: { area: 'admin' },
children: [
{ path: 'users/:userId', component: OrgUser }, // OrgUser gets orgId + userId + area
],
}
// The old behaviour (inherit only from parents without a component / with an empty path):
provideRouter(routes, withRouterConfig({ paramsInheritanceStrategy: 'emptyOnly' }));If a child route has a parameter with the same name as a parent, or if you relied on a child NOT seeing its parent’s data, check after upgrading. For the old behaviour: paramsInheritanceStrategy: 'emptyOnly'.
Navigation and links
<a routerLink="/products">Products</a>
<a [routerLink]="['/products', product().id]">Details</a>
<a [routerLink]="['user', 2]" [queryParams]="{ tab: 'activitate' }">Relative + query</a>
<a routerLink="/products" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Exactly active</a>
<router-outlet />When YES, when NO
- A screen that needs its own link (shareable, bookmark, back button).
- Entity detail:
/products/42. - Important tabs you should be able to open directly:
/settings/security. - Shareable filters and pagination → query params.
- Ephemeral UI state: dropdown, hover, confirmation dialog.
- Wizard steps that make no sense opened directly.
- Small secondary panels →
@if+ signal.
Common pitfalls
- You forget
withComponentInputBinding()→ inputs stayundefined. - Params are strings: use
transform: numberAttributefor numeric ids. - The component isn’t recreated when navigating
/user/1→/user/2(same route): that’s why the inputs are signals — everything recomputes on its own. - Eager routes (
component: Xinapp.routes.ts) for large pages → all the code in the initial bundle. Migration:ng g @angular/core:route-lazy-loading.