Skip to content
Back to skills

Angular Routing Architecture

ASecurity

Reviews Angular Router architecture beyond permission guards (owned by angular-security) — resolvers, typed route data, nested composition. Flags detail data fetched in ngOnInit instead of a Resolver, magic-string route data, deep routes with no lazy boundary, missing wildcard/redirect strategy, guards duplicated across siblings instead of applied at a parent, and components ignoring paramMap changes. Outputs pilot-angular routing-architecture standard IDs.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 6, 2026
developmenttypescriptgoshellreactangularapisecurity

Works with

  • api

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add AgenticPawan/FullStack-Pilot --skill angular-routing-architecture --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Angular Routing Architecture?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Angular Routing Architecture
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/agenticpawan-angular-routing-architecture/badge)](https://www.skillsdirectory.com/skills/agenticpawan-angular-routing-architecture)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: angular-routing-architecture
description: Reviews Angular Router architecture beyond permission guards (owned by angular-security) — resolvers, typed route data, nested composition. Flags detail data fetched in ngOnInit instead of a Resolver, magic-string route data, deep routes with no lazy boundary, missing wildcard/redirect strategy, guards duplicated across siblings instead of applied at a parent, and components ignoring paramMap changes. Outputs pilot-angular routing-architecture standard IDs.
when_to_use: Resolver, route resolver, route data, breadcrumb, route title, lazy loading route, loadChildren, loadComponent, wildcard route, 404 route, 403 route, canActivateChild, paramMap, snapshot params, sibling route navigation, nested routes, route architecture
applies_to: angular
---

<!-- Version index:
  Functional guards/resolvers (ResolveFn)   Angular 14.2+
  loadComponent (standalone lazy routes)    Angular 15+
  withComponentInputBinding()               Angular 16+ (route data/params bound to component inputs)
-->

## Standard IDs

| ID | Severity | What it checks |
|----|----------|-----------------|
| RTA-001 | P1 | Detail data fetched in `ngOnInit` with a loading spinner instead of a Resolver |
| RTA-002 | P2 | Route data (title, breadcrumb, icon, permission key) as scattered magic strings |
| RTA-003 | P1 | Deeply nested feature routes with no lazy-loading boundary per feature |
| RTA-004 | P1 | No consistent wildcard/redirect strategy for unmatched or unauthorized routes |
| RTA-005 | P2 | `CanActivate` guard duplicated across sibling routes instead of applied once at a parent |
| RTA-006 | P1 | Route params read via snapshot only — component doesn't react to sibling-route param changes |

**Cross-reference:** permission/role-based `CanActivate`/`CanMatch` guard *content* (permission
vs. role checks) is owned by `angular-security` (rule `angular-permission-based-authz`). This
skill governs *where* guards are applied (parent vs. duplicated per sibling) and the surrounding
router architecture — resolvers, route data, lazy-loading boundaries, and param reactivity.

---

## Check A — Resolvers instead of component-driven data fetching (RTA-001)

### Detection
1. Scan components rendered by a route (i.e. referenced by `loadComponent`/`component` in a
   `Routes` array) for an `ngOnInit` that issues an HTTP call and toggles a `loading` flag while
   the template renders a spinner.
2. Flag this pattern when the component immediately needs the data to render anything meaningful
   — the route activates into a half-rendered shell before the fetch resolves.
3. A documented, deliberate choice to defer to component-level fetching for skeleton-loading UX
   (e.g. a list view that renders a skeleton grid while paginated data streams in) is acceptable —
   only flag when there is no such documented rationale and a Resolver would be the natural fit
   (single-entity detail views, edit forms).

### BAD — component fetches its own detail data with a manual loading flag
```typescript
// order-detail.component.ts
export class OrderDetailComponent implements OnInit {
  private route = inject(ActivatedRoute);
  private orderService = inject(OrderService);
  order?: Order;
  loading = true;

  ngOnInit() {
    const id = this.route.snapshot.paramMap.get('id')!;
    this.orderService.getById(id).subscribe(order => {
      this.order = order;
      this.loading = false;
    });
  }
}
```
```html
@if (loading) { <app-spinner /> } @else { <app-order-summary [order]="order!" /> }
```

### GOOD — Resolver guarantees data before the route activates
```typescript
// order.resolver.ts
export const orderResolver: ResolveFn<Order> = (route) => {
  const orderService = inject(OrderService);
  return orderService.getById(route.paramMap.get('id')!);
};
```
```typescript
// orders.routes.ts
export const ordersRoutes: Routes = [
  {
    path: ':id',
    loadComponent: () => import('./order-detail.component').then(m => m.OrderDetailComponent),
    resolve: { order: orderResolver }
  }
];
```
```typescript
// order-detail.component.ts — Angular 16+ withComponentInputBinding binds resolved data directly
export class OrderDetailComponent {
  order = input.required<Order>();
}
```

---

## Check B — Typed route data convention (RTA-002)

### Detection
1. Search route configs for `data: { ... }` usage across the app.
2. Flag inconsistency: some routes set `data: { title: '...' }`, others set a differently-named
   key (`pageTitle`, `label`), and some routes set no `data` at all — with title/breadcrumb/icon
   values instead hardcoded per component template.
3. Confirm a shared `RouteData` interface exists and a single service (title service, breadcrumb
   service) reads from it consistently via `router.routerState.root` traversal, rather than each
   component re-deriving its own title/breadcrumb string.

### BAD — magic strings, inconsistent keys, some components hardcode their own title
```typescript
// orders.routes.ts
{ path: 'orders', data: { title: 'Orders' }, loadComponent: () => import('./orders-list.component') }
{ path: 'invoices', data: { pageTitle: 'Invoices', crumb: 'Invoices' }, loadComponent: () => import('./invoices.component') }
```
```typescript
// invoices.component.ts — sets document.title itself, bypassing route data entirely
ngOnInit() { document.title = 'Invoices — Contoso'; }
```

### GOOD — a shared typed RouteData convention read by one TitleService/BreadcrumbService
```typescript
// route-data.model.ts
export interface RouteData {
  title: string;
  breadcrumb: string;
  icon?: string;
  permissionKey?: string;
}
```
```typescript
// orders.routes.ts
{
  path: 'orders',
  data: { title: 'Orders', breadcrumb: 'Orders', icon: 'list', permissionKey: 'orders.view' } satisfies RouteData,
  loadComponent: () => import('./orders-list.component').then(m => m.OrdersListComponent)
}
```
```typescript
// breadcrumb.service.ts — single place that walks routerState and reads RouteData
@Injectable({ providedIn: 'root' })
export class BreadcrumbService {
  private router = inject(Router);
  breadcrumbs = toSignal(
    this.router.events.pipe(
      filter(e => e instanceof NavigationEnd),
      map(() => this.buildTrail(this.router.routerState.snapshot.root))
    ),
    { initialValue: [] as RouteData[] }
  );
}
```

---

## Check C — Lazy-loading boundaries in nested feature routes (RTA-003)

### Detection
1. Map the route tree depth for each feature area. Identify any nested child route set
   (2+ levels deep) that is imported eagerly via a top-level array literal rather than
   `loadChildren`/`loadComponent`.
2. Flag a feature whose entire sub-tree (list, detail, edit, nested tabs) is statically imported
   into the root route config — this forces the whole feature into the initial/eagerly-loaded
   bundle regardless of whether the user ever navigates there.
3. Confirm each top-level feature boundary uses `loadChildren: () => import('./feature.routes')`
   (or per-route `loadComponent`) so bundle splitting occurs at a natural feature seam.

### BAD — entire nested feature tree statically imported at the root
```typescript
// app.routes.ts
import { OrdersListComponent } from './features/orders/orders-list.component';
import { OrderDetailComponent } from './features/orders/order-detail.component';
import { OrderLineItemsComponent } from './features/orders/order-line-items.component';
import { OrderShipmentsComponent } from './features/orders/order-shipments.component';

export const routes: Routes = [
  {
    path: 'orders',
    children: [
      { path: '', component: OrdersListComponent },
      {
        path: ':id',
        component: OrderDetailComponent,
        children: [
          { path: 'items', component: OrderLineItemsComponent },
          { path: 'shipments', component: OrderShipmentsComponent }
        ]
      }
    ]
  }
];
```

### GOOD — lazy boundary at the feature seam, nested routes defined in their own file
```typescript
// app.routes.ts
export const routes: Routes = [
  { path: 'orders', loadChildren: () => import('./features/orders/orders.routes').then(m => m.ordersRoutes) }
];
```
```typescript
// features/orders/orders.routes.ts
export const ordersRoutes: Routes = [
  { path: '', loadComponent: () => import('./orders-list.component').then(m => m.OrdersListComponent) },
  {
    path: ':id',
    loadComponent: () => import('./order-detail.component').then(m => m.OrderDetailComponent),
    children: [
      { path: 'items', loadComponent: () => import('./order-line-items.component').then(m => m.OrderLineItemsComponent) },
      { path: 'shipments', loadComponent: () => import('./order-shipments.component').then(m => m.OrderShipmentsComponent) }
    ]
  }
];
```

---

## Check D — Wildcard/redirect strategy for unmatched and unauthorized paths (RTA-004)

### Detection
1. Confirm a top-level `{ path: '**', ... }` route exists rendering a dedicated 404 component,
   not an empty/blank fallback.
2. Confirm routes protected by a permission guard have a defined redirect (e.g. to a 403 page or
   login) when the guard denies access — a guard that returns `false` with no `UrlTree` redirect
   leaves the router on a blank screen with no explanation.
3. Flag any app with no `**` route at all, or a `**` route that just redirects to `/` silently
   (masking genuinely broken links instead of surfacing a 404).

### BAD — no wildcard route; denied guard returns bare false
```typescript
// app.routes.ts — no ** route: unmatched URLs render nothing inside <router-outlet>
export const routes: Routes = [
  { path: 'orders', loadChildren: () => import('./features/orders/orders.routes') }
];
```
```typescript
// permission.guard.ts
export const permissionGuard: CanActivateFn = (route) => {
  return inject(PermissionService).hasPermission(route.data['permissionKey']); // false = blank page
};
```

### GOOD — explicit 404/403 handling
```typescript
// app.routes.ts
export const routes: Routes = [
  { path: 'orders', loadChildren: () => import('./features/orders/orders.routes') },
  { path: '403', loadComponent: () => import('./shared/forbidden.component').then(m => m.ForbiddenComponent) },
  { path: '**', loadComponent: () => import('./shared/not-found.component').then(m => m.NotFoundComponent) }
];
```
```typescript
// permission.guard.ts
export const permissionGuard: CanActivateFn = (route) => {
  const hasPermission = inject(PermissionService).hasPermission(route.data['permissionKey']);
  return hasPermission ? true : inject(Router).createUrlTree(['/403']);
};
```

---

## Check E — Guards applied once at the parent, not per sibling (RTA-005)

### Detection
1. Look for the same `CanActivate` function repeated across multiple sibling route entries that
   share a common parent (e.g. every child under `orders/:id/...` re-declaring the same
   permission guard).
2. Flag this duplication and recommend hoisting the guard to the parent route via `canActivate`
   on the parent, or `canActivateChild` when the parent itself has no component to guard.

### BAD — same guard repeated on every sibling
```typescript
export const ordersRoutes: Routes = [
  { path: 'summary', canActivate: [authGuard], loadComponent: () => import('./summary.component') },
  { path: 'items', canActivate: [authGuard], loadComponent: () => import('./items.component') },
  { path: 'shipments', canActivate: [authGuard], loadComponent: () => import('./shipments.component') }
];
```

### GOOD — guard hoisted once via canActivateChild
```typescript
export const ordersRoutes: Routes = [
  {
    path: '',
    canActivateChild: [authGuard],
    children: [
      { path: 'summary', loadComponent: () => import('./summary.component') },
      { path: 'items', loadComponent: () => import('./items.component') },
      { path: 'shipments', loadComponent: () => import('./shipments.component') }
    ]
  }
];
```

---

## Check F — Reacting to param changes between sibling routes (RTA-006)

### Detection
1. Find components read by routes with a dynamic segment (`:id`) that are also reachable via a
   sibling-to-sibling navigation of the *same* route (e.g. a "next order" link that navigates
   from `/orders/1` to `/orders/2` without the component being destroyed/recreated, since Angular
   reuses the component instance by default).
2. Flag any such component that reads `route.snapshot.paramMap.get('id')` only in `ngOnInit` —
   it will show stale data for the new id because `ngOnInit` does not re-run on a same-component
   sibling navigation.
3. Confirm the component instead subscribes to `route.paramMap` (an `Observable`) or uses
   `toSignal(route.paramMap)`/Angular 16+ `withComponentInputBinding()` bound inputs, which do
   update on every navigation.

### BAD — snapshot read once in ngOnInit; stale on sibling navigation
```typescript
export class OrderDetailComponent implements OnInit {
  private route = inject(ActivatedRoute);
  order?: Order;

  ngOnInit() {
    const id = this.route.snapshot.paramMap.get('id')!; // captured once, never updates
    this.orderService.getById(id).subscribe(o => this.order = o);
  }
}
```

### GOOD — reactive paramMap subscription (or bound input) reloads on every navigation
```typescript
export class OrderDetailComponent {
  private route = inject(ActivatedRoute);
  private orderService = inject(OrderService);

  id = toSignal(this.route.paramMap.pipe(map(params => params.get('id')!)));
  order = toSignal(
    toObservable(this.id).pipe(switchMap(id => this.orderService.getById(id)))
  );
}
```
```typescript
// Angular 16+: withComponentInputBinding() binds :id directly as a component input,
// which updates automatically on sibling-route navigation
export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes, withComponentInputBinding())]
};
```

---

## Routing architecture checklist

- [ ] Single-entity detail/edit routes use a Resolver rather than a component `ngOnInit` fetch-and-spin
- [ ] Deliberate component-driven fetching (skeleton UX) is documented, not accidental
- [ ] All route `data:` blocks follow one typed `RouteData` shape (title, breadcrumb, icon, permissionKey)
- [ ] A shared title/breadcrumb service reads `data` — no per-component `document.title` hacks
- [ ] Every top-level feature area has a `loadChildren`/`loadComponent` lazy-loading boundary
- [ ] A `**` wildcard route renders a dedicated 404 page
- [ ] Denied permission guards redirect to a 403 page/login, never return bare `false`
- [ ] Guards shared by sibling routes are hoisted to the parent via `canActivate`/`canActivateChild`
- [ ] Components reachable via sibling-route navigation react to `paramMap` changes, not just `ngOnInit` snapshot reads

---

## References

- Angular Router — resolvers: https://angular.dev/guide/routing/common-router-tasks#resolve-dynamic-data-with-the-router
- Angular Router — route data & component input binding: https://angular.dev/api/router/withComponentInputBinding
- Angular Router — lazy loading: https://angular.dev/guide/ngmodules/lazy-loading
- Angular Router — redirect strategies: https://angular.dev/guide/routing/common-router-tasks#displaying-a-404-page

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…