When agents use itUse 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.
Install
Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.
npx skills add AGCO-Global/org-skills --skill angular-architecture -a github-copilot
# or with the org installer (adds .github/skills/angular-architecture):
npx -y github:AGCO-Global/org-skills add skill angular-architecture
Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.
Installs the frontend-angular-skills plugin, which bundles all Frontend / Angular skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install frontend-angular-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill angular-architecture -a claude-code
Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.
Installs into .agents/skills/, which Codex reads.
npx skills add AGCO-Global/org-skills --skill angular-architecture -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill angular-architecture -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill angular-architecture -a gemini-cli
Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.
Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.
Skill contents
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
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
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:
Upgrade to the latest LTS with ng update, one major at a time.
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 BehaviorSubjects 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.
---
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.