---
name: angular-architecture
description: Architect-level guidance for modern Angular (v17+) applications — standalone components, signals-based state, feature-sliced folder structure, dependency injection design, routing with lazy loading, RxJS boundaries, Nx monorepos, and migrating legacy NgModule/zone.js codebases. Use this whenever the user is starting or restructuring an Angular app, choosing between NgRx / SignalStore / plain signal services, asking how to organise modules or libraries, deciding on Angular Material vs PrimeNG vs custom design system, planning a migration to standalone/signals/zoneless, or asks "how should I structure this" for anything Angular. Also apply it when reviewing Angular code for architectural problems.
metadata:
  technology: Angular
  type: architecture
---

# Angular Architecture

Modern Angular is standalone components, signals, and functional DI. If the user's codebase is NgModules + heavy RxJS-in-templates + `any`, the architecture conversation is a migration conversation — say so and plan it.

## 1. Baseline decisions (record in ARCHITECTURE.md)

| Decision | Default | Change when |
|---|---|---|
| Components | **Standalone**, `ChangeDetectionStrategy.OnPush`, signals for inputs/outputs (`input()`, `output()`, `model()`) | Never for new code. |
| Change detection | **Zoneless** (`provideZonelessChangeDetection()`) on new apps; OnPush + signals everywhere | Legacy libs depending on zone.js → keep zone until they're replaced; still OnPush. |
| State | **Signals in services** for local/feature state; **NgRx SignalStore** for shared feature state; **NgRx Store** only for large event-sourced domains | Small app → signals only. |
| Async | **RxJS at the edges** (HTTP, WebSockets, router events, complex event streams), convert to signals with `toSignal()` for templates | Never subscribe manually in components when `toSignal`/`AsyncPipe` works. |
| HTTP | `HttpClient` with functional interceptors; `HttpResource` / `httpResource()` where the version supports it | — |
| Forms | **Typed Reactive Forms** (or Signal Forms when stable) | Template-driven forms only for trivial cases. |
| UI library | **Angular Material + CDK** (tokens via M3 theming) or **PrimeNG** for data-heavy enterprise | Existing design system → CDK primitives + custom components. |
| Tooling | Angular CLI with **esbuild/Vite builder** (`application` builder), **Nx** when > 1 app or shared libs | — |
| Testing | Vitest (via `@angular/build:unit-test`) or Jest, Testing Library, Playwright | Karma is deprecated — migrate. |
| SSR | `@angular/ssr` with hydration and incremental hydration for public/SEO apps | Internal apps: CSR is fine. |

## 2. Folder structure — feature-first with explicit public APIs

```
src/app/
├── app.config.ts          # providers: router, http, zoneless, i18n
├── app.routes.ts          # top-level lazy routes only
├── core/                  # singletons: auth, http interceptors, error handler, config, logger
│   ├── auth/
│   ├── http/
│   └── layout/            # shell, nav — one place
├── features/
│   └── orders/
│       ├── orders.routes.ts       # lazy-loaded route file
│       ├── pages/                 # routed components (smart)
│       ├── components/            # feature-private presentational
│       ├── data/                  # OrdersApi (HttpClient), OrdersStore (signals/SignalStore)
│       ├── model/                 # types, pure functions
│       └── index.ts               # public API
├── shared/
│   ├── ui/                # dumb reusable components, directives, pipes
│   ├── util/              # pure helpers
│   └── data-access/       # cross-feature API clients only if truly shared
└── entities/              # cross-feature domain models
```

Rules:
- Features import from `shared/`, `core/`, `entities/`, and other features' `index.ts` only. Enforce with `@nx/enforce-module-boundaries` or `eslint-plugin-boundaries`.
- `core/` is provided once at root; nothing in `core/` imports from `features/`.
- Pages are the only components that inject stores/APIs; everything under `components/` receives `input()` and emits `output()`.
- One component per file; `Component` suffix; `.component.ts`/`.component.html`/`.component.css` or inline template for small components.

In an Nx monorepo the same layers become library types: `feature-*`, `ui-*`, `data-access-*`, `util-*`, tagged and boundary-enforced.

## 3. Component design

- **Inputs/outputs as signals**: `readonly user = input.required<User>()`, `readonly save = output<User>()`, two-way with `model()`.
- **Derive with `computed()`**, never store derived values in fields updated by `effect()`.
- **`effect()` is for side effects only** (logging, syncing to localStorage, imperative DOM). If you're calling `.set()` inside an effect, you almost certainly want `computed()` or `linkedSignal()`.
- **Control flow**: `@if`, `@for` (with mandatory `track`), `@switch`, `@defer` for below-the-fold heavy components.
- **Host bindings** via `host: {}` in the decorator, not `@HostBinding`/`@HostListener` sprinkled through the class.
- **`inject()`** in field initialisers, not constructor parameters. Enables composition into functions (`injectAuthUser()`).
- **Content projection** with `ng-content select` and `contentChild()` signals for compound components (`<app-card><app-card-header/>…`).
- **Directives for cross-cutting behaviour** (tooltip, autofocus, permission gating) instead of wrapping components.

## 4. State management — pick by scope

| Scope | Pattern |
|---|---|
| Component-local | `signal()` / `computed()` in the component |
| Feature, shared by a few components | Service with signals, `providedIn: 'root'` or provided at the route level for per-feature lifetime |
| Feature, with async loading, entities, and methods | **NgRx SignalStore** (`signalStore`, `withState`, `withComputed`, `withMethods`, `rxMethod`, `withEntities`) |
| Server data cache | `httpResource()` / `resource()` where available; otherwise a small query layer (TanStack Query for Angular is viable) |
| URL state | Router params/query params, read via `input()` with `withComponentInputBinding()` |
| Forms | Reactive Forms — the form *is* the state |
| App-wide, event-sourced, audited | NgRx Store with actions/reducers/effects — justify it in writing |

Rule of thumb: a store per feature, provided at the feature route, destroyed on leave. Root-provided global stores only for session/auth/preferences.

## 5. RxJS boundaries

RxJS is for *streams of events over time*; signals are for *state right now*. Keep them apart:

- HTTP: `HttpClient` returns Observables — map into a signal with `toSignal(obs$, { initialValue })` or feed a SignalStore `rxMethod`.
- Complex user-event orchestration (typeahead with debounce/switchMap/cancellation) — RxJS pipeline, `toSignal` at the end.
- Router, WebSocket, DOM event streams — RxJS.
- Templates never contain `| async` chains on freshly created Observables; never `subscribe()` in components without `takeUntilDestroyed()`; never nested subscribes.
- Prefer `switchMap` for cancellable requests, `exhaustMap` for submit buttons, `mergeMap` only when parallelism is intentional.

## 6. Dependency injection design

- Functional providers (`provideRouter`, `provideHttpClient(withInterceptors([...]))`) in `app.config.ts`.
- `InjectionToken` with `providedIn: 'root'` factories for configuration and abstraction points (`API_BASE_URL`, `LOGGER`).
- Route-level providers for feature-scoped services (`providers: [OrdersStore]` in the route).
- Abstract classes as tokens when there are multiple implementations (e.g. `StorageService` with `LocalStorageService`/`MemoryStorageService` for tests).
- Functional guards, resolvers, and interceptors (`CanActivateFn`, `ResolveFn`, `HttpInterceptorFn`).

## 7. Routing

- Every feature is a `loadChildren` lazy route pointing at `<feature>.routes.ts`.
- `withComponentInputBinding()` so route params arrive as `input()` signals.
- Guards for auth/permissions; resolvers only for data that *must* exist before render — otherwise render a skeleton and load in the component.
- Preloading strategy: `PreloadAllModules` for small apps, custom (on hover/visible) for large.
- Route-level `title` and `data` for breadcrumbs and analytics; typed via a shared `RouteData` interface.

## 8. Migration playbook (legacy → modern)

Sequence matters; each step ships independently:

1. Upgrade to the latest LTS with `ng update`, one major at a time.
2. `ng generate @angular/core:standalone` — three-step schematic (components → remove modules → bootstrap).
3. Switch builder to `@angular/build:application` (esbuild).
4. `ng generate @angular/core:control-flow` for `@if`/`@for`.
5. `ng generate @angular/core:signal-input-migration`, `output-migration`, `inject-migration`.
6. Set `OnPush` everywhere; fix what breaks (it reveals hidden mutation).
7. Replace subject-based services with signals / SignalStore, feature by feature.
8. Remove zone.js: `provideZonelessChangeDetection()`, remove `zone.js` polyfill, fix libs.
9. Karma → Vitest/Jest.

Track each step in a checklist in the repo; measure bundle size and CD cycles before/after.

## 9. Non-negotiables checklist

- [ ] All new components standalone + OnPush + signal inputs
- [ ] `@for` always has `track`
- [ ] No `subscribe()` without `takeUntilDestroyed()` or `toSignal`
- [ ] Features lazy-loaded; boundaries enforced by lint
- [ ] `strict: true`, `strictTemplates: true`, `noImplicitOverride`
- [ ] Every route has a loading/skeleton state and an error path
- [ ] ESLint with `@angular-eslint` (template a11y rules on) in CI
- [ ] Budgets configured in `angular.json` and enforced in CI

## Anti-patterns to flag

- `NgModule` for new code; `SharedModule` that exports everything.
- `effect()` writing to signals — use `computed`/`linkedSignal`.
- Storing HTTP results in `BehaviorSubject`s and exposing `.value` everywhere.
- Business logic in components; components calling `HttpClient` directly.
- `any` in templates; `strictTemplates` off.
- Constructor injection with 8 dependencies — the component is doing too much.
- `ngOnChanges` implementing what `computed()` on inputs does.
- `*ngIf` / `*ngFor` in new templates.
