Code Reviewer — Angular Overlay
This skill extends code-reviewer (the universal skill). Always apply the universal
skill's full checklist first, then apply the Angular-specific rules in this file on top.
Composition order:
- Run
code-reviewer (universal pillars: correctness, security, performance, DRY, tests, docs)
- Run this overlay (Angular/TypeScript-specific rules below)
- Report findings from both in a single unified output
Step 0 — Detect Versions First (always, before reviewing anything)
Run these commands before touching any code. Version determines which rules apply.
# Angular and CLI version
cat package.json | grep -E '"(@angular/core|@angular/cli|@angular/material|@ngrx|@apollo/client|apollo-angular|typescript|rxjs)"' | head -20
# Check if zoneless is configured
grep -r "provideZoneless\|provideExperimentalZoneless\|zone.js" src/ --include="*.ts" -l 2>/dev/null | head -5
# Check standalone vs module-based
grep -r "NgModule\|standalone:" src/ --include="*.ts" -l 2>/dev/null | head -10
# Check for legacy structural directives still in use
grep -r "\*ngIf\|\*ngFor\|\*ngSwitch" src/ --include="*.html" -l 2>/dev/null | head -5
Report at the top of your review:
🔍 Environment: Angular vX.Y | TypeScript X.Y | RxJS X.Y
NgRx: X.Y (if present) | Apollo Angular: X.Y (if present)
Mode: Standalone / Module-based / Mixed | Zoneless: Yes / No / Not yet
Then apply the version-specific rules below.
Angular Version Rules
Angular 17
- New control flow (
@if, @for, @switch) introduced — flag new code still using *ngIf/*ngFor; suggest migration.
standalone: true is now the default recommended approach — flag new NgModule-based components without justification.
@for requires track — flag any @for loop missing track user.id (or equivalent stable key). Never track $index on mutable lists.
- Deferrable views (
@defer) available — flag large components that could benefit from deferred loading.
- TypeScript 5.2+ required — flag
tsconfig targeting lower versions.
Angular 18
- Signals API stable — flag
BehaviorSubject used for simple local/component state; suggest signal().
- Zoneless change detection available as experimental (
provideExperimentalZonelessChangeDetection) — don't flag usage, but flag missing OnPush on new components regardless.
- esbuild + Vite is the default builder — flag
angular.json still using webpack (@angular-devkit/build-angular:browser) for new projects.
- Angular Material 3 stable — flag Material 2 component APIs that were renamed.
Angular 19
standalone: true is the default — flag @Component({ standalone: false }) unless the component is intentionally module-based for a documented reason. Flag components declared in NgModule.declarations — Angular 19 will throw NG2007.
httpResource() available (experimental, 19.2+) — acceptable for component-level data fetching; flag it being used in service layers (keep services using HttpClient).
- ⚠️ EOL May 19, 2026 — flag projects pinned to Angular 19 as High priority. Suggest immediate migration to Angular 20.
- XSS CVE fixed in 19.2.18 (SVG
href/xlink:href bypass) — flag projects on Angular 19 < 19.2.18 as Critical.
Angular 20+ ✅ Current recommended
- Signals fully stable —
effect(), linkedSignal(), input(), output(), model() all stable.
ngIf, ngFor, ngSwitch are deprecated — flag any new code using structural directives; flag existing code as Medium priority for migration.
provideZonelessChangeDetection (renamed from provideExperimentalZonelessChangeDetection) — flag old name as breaking in 20.
- TypeScript 5.8+ required, Node 20+ required — flag
package.json engines or tsconfig.json targeting lower versions.
- View Engine metadata completely removed — flag any library dependency still referencing View Engine.
Upcoming (Angular 21):
- Signal Forms expected — once available, flag
FormGroup/FormControl in new Angular 21+ projects; suggest Signal Forms.
Angular-Specific Review Checklist
🏗️ Components
OnPush change detection — flag every component missing changeDetection: ChangeDetectionStrategy.OnPush. This is non-negotiable for performance-conscious teams.
// ❌ Default change detection — scans entire tree
@Component({ selector: 'app-user' })
// ✅ OnPush — only updates when inputs change or signals notify
@Component({
selector: 'app-user',
changeDetection: ChangeDetectionStrategy.OnPush,
})
inject() function — flag constructor injection in new code; use inject() exclusively:
// ❌ Constructor injection (legacy)
constructor(private readonly userService: UserService) {}
// ✅ inject() function
private readonly userService = inject(UserService);
Signal inputs/outputs (v17.1+) — flag @Input()/@Output() + EventEmitter in new components on Angular 17+; prefer input(), output(), model():
// ❌ Legacy decorator inputs
@Input({ required: true }) user!: User;
@Output() selected = new EventEmitter<User>();
// ✅ Signal-based
readonly user = input.required<User>();
readonly selected = output<User>();
subscribe() in components — flag direct .subscribe() calls in component code; use toSignal() or async pipe instead:
// ❌ Manual subscription — memory leak risk
ngOnInit() {
this.userService.getUser().subscribe(u => this.user = u);
}
// ✅ toSignal — auto-cleaned up
readonly user = toSignal(this.userService.getUser());
@for track — flag @for loops missing a stable track expression:
<!-- ❌ Missing track — full DOM re-render on any change -->
@for (user of users()) {
<app-user-card [user]="user" />
}
<!-- ✅ Stable key -->
@for (user of users(); track user.id) {
<app-user-card [user]="user" />
}
Template safety — flag [innerHTML] bindings without DomSanitizer. Flag bypassSecurityTrustHtml() / bypassSecurityTrustUrl() without documented justification — these bypass Angular's XSS protection.
CommonModule imports — flag CommonModule in standalone component imports; use specific imports (NgIf, NgFor, AsyncPipe) or preferably the new control flow syntax instead.
NgModule in new features — flag new NgModule creation on Angular 17+; all new code should be standalone.
⚡ Signals
signal() for local state — flag BehaviorSubject used for component or feature-local state where signal() would suffice.
computed() for derived state — flag computed values re-derived manually in multiple places; extract to computed():
// ❌ Manual derivation, duplicated
get activeUsers() { return this.users().filter(u => u.active); }
// ✅ computed — memoized, reactive
readonly activeUsers = computed(() => this.users().filter(u => u.active));
effect() side effects — flag effect() used to derive or transform state (that's computed()'s job). effect() is for side effects only (logging, localStorage, external calls).
signal.asReadonly() — flag writable signals exposed publicly from services; expose asReadonly():
// ❌ Writable signal leaked — consumers can mutate
readonly users = signal<User[]>([]);
// ✅ Expose read-only; mutate only through service methods
private readonly _users = signal<User[]>([]);
readonly users = this._users.asReadonly();
toSignal() with initialValue — flag toSignal() calls on Observables that may not emit immediately without an initialValue or { requireSync: true } — results in undefined signal until first emission.
linkedSignal() (v19+) — flag complex computed() + effect() combinations that reset a signal when another changes; linkedSignal() is the cleaner pattern.
🔁 RxJS
Rule of thumb: RxJS for async/events, Signals for state. Convert at component boundary with toSignal().
Unsubscribed observables — flag .subscribe() without cleanup. Acceptable patterns: takeUntilDestroyed(), async pipe, toSignal(). Flag ngOnDestroy + manual Subscription arrays in new code — use takeUntilDestroyed(this.destroyRef) instead:
// ❌ Manual unsubscribe boilerplate
private sub = new Subscription();
ngOnInit() { this.sub.add(obs$.subscribe(...)); }
ngOnDestroy() { this.sub.unsubscribe(); }
// ✅ takeUntilDestroyed
obs$.pipe(takeUntilDestroyed()).subscribe(...);
switchMap for cancellable requests — flag mergeMap or concatMap on search/autocomplete streams where only the latest result matters.
Error handling — flag Observable chains missing catchError — unhandled errors complete the stream and break the UI.
BehaviorSubject as state — flag BehaviorSubject used for state that is: (a) local to a component, (b) synchronous, (c) not shared across features. Replace with signal().
Nested subscriptions — flag .subscribe() inside another .subscribe() — use switchMap, mergeMap, or combineLatest instead.
🗺️ Routing & Lazy Loading
Lazy loading — flag loadComponent / loadChildren not used for feature routes. Every feature route should be lazy-loaded:
// ❌ Eager — entire module loaded upfront
{ path: 'users', component: UsersComponent }
// ✅ Lazy — loaded on demand
{ path: 'users', loadComponent: () => import('./users/users.component') }
Functional guards — flag class-based CanActivate / CanDeactivate guards in Angular 15+ projects; use functional guards:
// ❌ Class-based guard (legacy)
@Injectable()
export class AuthGuard implements CanActivate { ... }
// ✅ Functional guard
export const authGuard: CanActivateFn = (route, state) => {
return inject(AuthService).isAuthenticated()
? true
: inject(Router).createUrlTree(['/login']);
};
input.fromRoute() (v17.1+) — flag components using ActivatedRoute.snapshot.params or paramMap subscriptions; prefer input.fromRoute() where route inputs are enabled.
withComponentInputBinding() — flag projects not enabling route component input binding in provideRouter() — required for input.fromRoute() to work.
Route prefetching — flag critical user flows without prefetch strategy on lazy routes.
🌐 HttpClient & REST
Services, not components — flag HttpClient injected directly in a component; all HTTP calls belong in services.
httpResource() (v19.2+) — acceptable for component-level reactive data fetching. Flag it inside services (use HttpClient there).
Error handling — flag HTTP calls without catchError or error state handling in the UI.
Typed HTTP responses — flag http.get('/api/users') without a type parameter; always use http.get<User[]>('/api/users').
Interceptors — flag class-based HttpInterceptor in Angular 15+ projects; use functional interceptors:
// ✅ Functional interceptor
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};
Retry logic — flag API calls that should retry on transient failures (network errors) without retry() or retryWhen().
🔐 Authentication (JWT / OAuth / Guards)
- Route guards — flag protected routes missing
canActivate with an auth guard.
- Token storage — flag JWT stored in
localStorage; prefer httpOnly cookies or sessionStorage with XSS mitigations.
- Token refresh — flag implementations that don't handle
401 responses with token refresh logic in an interceptor.
isAuthenticated as signal — flag auth state exposed as Observable where signal() + toSignal() would be cleaner for template consumption.
- Role-based access — flag guards checking only authentication, not authorization (roles/permissions) for admin or privileged routes.
- OAuth
state parameter — flag OAuth redirect flows missing CSRF state validation.
📦 NgRx
Tiered state rule — flag NgRx used for purely local component state; use signal() instead. NgRx is for: global state, cross-feature shared data, and complex side effects.
NgRx Signal Store (v17+) — flag class-based @ngrx/store for new features in modern Angular apps; prefer signalStore():
// ✅ NgRx Signal Store pattern
export const UsersStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed(({ users }) => ({
activeUsers: computed(() => users().filter(u => u.active))
})),
withMethods((store, usersService = inject(UsersService)) => ({
loadUsers: rxMethod<void>(
pipe(
switchMap(() => usersService.getAll()),
tapResponse({
next: users => patchState(store, { users }),
error: console.error
})
)
)
}))
);
Effects — flag side effects (HTTP calls, routing, notifications) placed directly in reducers or components; use NgRx Effects or withMethods + rxMethod.
Selectors — flag repeated store.select() calls for the same data; extract to reusable selectors.
patchState — flag direct state mutation attempts; always use patchState() in Signal Store methods.
🎨 Angular Material
- Theming — flag direct color values in component styles that should use Material theme tokens.
MatFormFieldModule — flag form fields missing proper appearance attribute.
- Accessibility — flag
mat-icon buttons missing matTooltip or aria-label.
- Module vs standalone imports — flag importing entire
MatButtonModule where only MatButton directive is needed (standalone tree-shaking).
🖥️ Angular Universal / SSR
isPlatformBrowser() — flag direct window, document, or localStorage access without platform check; these crash during SSR:
// ❌ Crashes on server
ngOnInit() { localStorage.setItem('key', 'value'); }
// ✅ Platform-safe
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
localStorage.setItem('key', 'value');
}
}
afterNextRender() / afterRender() — flag ngAfterViewInit used for browser-only DOM operations in SSR apps; use afterNextRender() (runs only in browser).
TransferState — flag SSR apps making the same HTTP request on both server and client; use TransferState or httpResource() to cache server-fetched data.
Incremental hydration (v19+) — flag large, below-the-fold components not using @defer with hydration triggers.
Event replay — flag SSR apps on v18+ not enabling withEventReplay() in provideClientHydration() — user interactions before hydration are lost.
🔵 GraphQL / Apollo Angular (if present)
- Typed queries — flag Apollo
useQuery / gql calls without generated TypeScript types; use graphql-codegen.
watchQuery vs query — flag query() used where the UI needs live cache updates; use watchQuery().
- Loading/error states — flag components using Apollo without handling
loading and error states in the template.
fetchPolicy — flag missing fetchPolicy on queries where stale data is a concern.
- Mutations and cache — flag mutations not updating the Apollo cache (
update function or refetchQueries) when they modify list data.
Anti-Patterns Quick Reference
Flag these immediately when spotted:
| Anti-pattern |
Severity |
Fix |
*ngIf / *ngFor in new code (v17+) |
Medium |
Use @if / @for |
@for without track |
High |
Add track item.id |
subscribe() in component body |
High |
Use toSignal() or async pipe |
| Constructor injection |
Low |
Use inject() |
@Input() / @Output() in new code (v17.1+) |
Low |
Use input() / output() |
BehaviorSubject for local state |
Medium |
Use signal() |
effect() for state derivation |
High |
Use computed() |
Writable signal exposed from service |
Medium |
Use .asReadonly() |
NgModule for new features (v17+) |
Medium |
Use standalone components |
CommonModule in standalone imports |
Low |
Import specific directives |
Direct window/document in SSR app |
Critical |
Use isPlatformBrowser() |
| HTTP call in component |
Medium |
Move to service |
[innerHTML] without sanitizer |
Critical |
Use DomSanitizer or restructure |
Missing OnPush |
Medium |
Add ChangeDetectionStrategy.OnPush |
Nested .subscribe() |
High |
Use switchMap/combineLatest |
| Unsubscribed observable |
High |
Use takeUntilDestroyed() |
| Angular 19 < 19.2.18 |
Critical |
Update — XSS CVE |
| Angular 19 (EOL May 2026) |
High |
Migrate to Angular 20 |
Unified Output Format
Use the same format as code-reviewer (universal). Add an Angular context line:
🔍 Environment: Angular v20.x | TypeScript 5.8 | RxJS 7.x
NgRx Signal Store: v19.x | Apollo Angular: N/A
Mode: Standalone | Zoneless: Developer Preview enabled
## Code Review Summary
[... standard universal format ...]
### 🅰️ Angular-Specific Issues
[Issues found by this overlay, using the same severity/format as universal]
Behavior Rules (Angular-specific additions)
- Version first, always — never apply version rules without detecting Angular version. Angular 19 EOL and XSS CVE are Critical-level findings.
- Signals-first mindset for state, RxJS for streams — don't flag RxJS in service layers; do flag RxJS
BehaviorSubject replacing signal() in component state.
OnPush is always required — no exceptions for new components regardless of version.
- SSR is high-risk —
window/document/localStorage without platform guard is always Critical in SSR projects.
- Don't refactor working NgRx to Signals — if existing class-based NgRx works, flag as Low/suggestion only; don't gate a PR on it.
- Delegate deep security audits →
security-auditor skill if available.
1---2name: code-reviewer-angular3description: Angular/TypeScript-specific code review overlay. Extends the universal code-reviewer skill with Angular version-aware rules. Trigger when reviewing Angular components, services, directives, pipes, guards, resolvers, NgRx stores/effects, RxJS streams, Apollo Angular GraphQL, HttpClient calls, SSR (Angular Universal), or any .ts/.html file in an Angular project. Keywords: Angular, standalone, component, NgRx, signal, computed, effect, RxJS, Observable, HttpClient, inject(), OnPush, @if, @for, router, guard, resolver, lazy loading, Angular Material, SSR, hydration. Do NOT trigger for Node.js backend code in the same monorepo (use code-reviewer-node for that) or for plain TypeScript utilities with no Angular imports. (updated 2026-03-28)4license: MIT5---67# Code Reviewer — Angular Overlay89This skill extends `code-reviewer` (the universal skill). Always apply the universal10skill's full checklist first, then apply the Angular-specific rules in this file on top.1112**Composition order:**131. Run `code-reviewer` (universal pillars: correctness, security, performance, DRY, tests, docs)142. Run this overlay (Angular/TypeScript-specific rules below)153. Report findings from both in a single unified output1617---1819## Step 0 — Detect Versions First (always, before reviewing anything)2021Run these commands before touching any code. Version determines which rules apply.2223```bash24# Angular and CLI version25cat package.json | grep -E '"(@angular/core|@angular/cli|@angular/material|@ngrx|@apollo/client|apollo-angular|typescript|rxjs)"' | head -202627# Check if zoneless is configured28grep -r "provideZoneless\|provideExperimentalZoneless\|zone.js" src/ --include="*.ts" -l 2>/dev/null | head -52930# Check standalone vs module-based31grep -r "NgModule\|standalone:" src/ --include="*.ts" -l 2>/dev/null | head -103233# Check for legacy structural directives still in use34grep -r "\*ngIf\|\*ngFor\|\*ngSwitch" src/ --include="*.html" -l 2>/dev/null | head -535```3637Report at the top of your review:38```39🔍 Environment: Angular vX.Y | TypeScript X.Y | RxJS X.Y40 NgRx: X.Y (if present) | Apollo Angular: X.Y (if present)41 Mode: Standalone / Module-based / Mixed | Zoneless: Yes / No / Not yet42```4344Then apply the version-specific rules below.4546---4748## Angular Version Rules4950### Angular 1751- New control flow (`@if`, `@for`, `@switch`) introduced — flag new code still using `*ngIf`/`*ngFor`; suggest migration.52- `standalone: true` is now the default recommended approach — flag new `NgModule`-based components without justification.53- `@for` **requires `track`** — flag any `@for` loop missing `track user.id` (or equivalent stable key). Never `track $index` on mutable lists.54- Deferrable views (`@defer`) available — flag large components that could benefit from deferred loading.55- TypeScript 5.2+ required — flag `tsconfig` targeting lower versions.5657### Angular 1858- Signals API **stable** — flag `BehaviorSubject` used for simple local/component state; suggest `signal()`.59- Zoneless change detection available as experimental (`provideExperimentalZonelessChangeDetection`) — don't flag usage, but flag missing `OnPush` on new components regardless.60- esbuild + Vite is the default builder — flag `angular.json` still using webpack (`@angular-devkit/build-angular:browser`) for new projects.61- Angular Material 3 stable — flag Material 2 component APIs that were renamed.6263### Angular 1964- **`standalone: true` is the default** — flag `@Component({ standalone: false })` unless the component is intentionally module-based for a documented reason. Flag components declared in `NgModule.declarations` — Angular 19 will throw `NG2007`.65- `httpResource()` available (experimental, 19.2+) — acceptable for component-level data fetching; flag it being used in service layers (keep services using `HttpClient`).66- ⚠️ **EOL May 19, 2026** — flag projects pinned to Angular 19 as High priority. Suggest immediate migration to Angular 20.67- XSS CVE fixed in 19.2.18 (SVG `href`/`xlink:href` bypass) — flag projects on Angular 19 < 19.2.18 as Critical.6869### Angular 20+ ✅ Current recommended70- Signals fully stable — `effect()`, `linkedSignal()`, `input()`, `output()`, `model()` all stable.71- **`ngIf`, `ngFor`, `ngSwitch` are deprecated** — flag any new code using structural directives; flag existing code as Medium priority for migration.72- `provideZonelessChangeDetection` (renamed from `provideExperimentalZonelessChangeDetection`) — flag old name as breaking in 20.73- TypeScript 5.8+ required, Node 20+ required — flag `package.json` engines or `tsconfig.json` targeting lower versions.74- View Engine metadata completely removed — flag any library dependency still referencing View Engine.7576**Upcoming (Angular 21):**77- Signal Forms expected — once available, flag `FormGroup`/`FormControl` in new Angular 21+ projects; suggest Signal Forms.7879---8081## Angular-Specific Review Checklist8283### 🏗️ Components8485- **`OnPush` change detection** — flag every component missing `changeDetection: ChangeDetectionStrategy.OnPush`. This is non-negotiable for performance-conscious teams.86 ```ts87 // ❌ Default change detection — scans entire tree88 @Component({ selector: 'app-user' })8990 // ✅ OnPush — only updates when inputs change or signals notify91 @Component({92 selector: 'app-user',93 changeDetection: ChangeDetectionStrategy.OnPush,94 })95 ```9697- **`inject()` function** — flag constructor injection in new code; use `inject()` exclusively:98 ```ts99 // ❌ Constructor injection (legacy)100 constructor(private readonly userService: UserService) {}101102 // ✅ inject() function103 private readonly userService = inject(UserService);104 ```105106- **Signal inputs/outputs (v17.1+)** — flag `@Input()`/`@Output()` + `EventEmitter` in new components on Angular 17+; prefer `input()`, `output()`, `model()`:107 ```ts108 // ❌ Legacy decorator inputs109 @Input({ required: true }) user!: User;110 @Output() selected = new EventEmitter<User>();111112 // ✅ Signal-based113 readonly user = input.required<User>();114 readonly selected = output<User>();115 ```116117- **`subscribe()` in components** — flag direct `.subscribe()` calls in component code; use `toSignal()` or `async` pipe instead:118 ```ts119 // ❌ Manual subscription — memory leak risk120 ngOnInit() {121 this.userService.getUser().subscribe(u => this.user = u);122 }123124 // ✅ toSignal — auto-cleaned up125 readonly user = toSignal(this.userService.getUser());126 ```127128- **`@for` track** — flag `@for` loops missing a stable `track` expression:129 ```html130 <!-- ❌ Missing track — full DOM re-render on any change -->131 @for (user of users()) {132 <app-user-card [user]="user" />133 }134135 <!-- ✅ Stable key -->136 @for (user of users(); track user.id) {137 <app-user-card [user]="user" />138 }139 ```140141- **Template safety** — flag `[innerHTML]` bindings without `DomSanitizer`. Flag `bypassSecurityTrustHtml()` / `bypassSecurityTrustUrl()` without documented justification — these bypass Angular's XSS protection.142143- **`CommonModule` imports** — flag `CommonModule` in standalone component imports; use specific imports (`NgIf`, `NgFor`, `AsyncPipe`) or preferably the new control flow syntax instead.144145- **NgModule in new features** — flag new `NgModule` creation on Angular 17+; all new code should be standalone.146147### ⚡ Signals148149- **`signal()` for local state** — flag `BehaviorSubject` used for component or feature-local state where `signal()` would suffice.150151- **`computed()` for derived state** — flag computed values re-derived manually in multiple places; extract to `computed()`:152 ```ts153 // ❌ Manual derivation, duplicated154 get activeUsers() { return this.users().filter(u => u.active); }155156 // ✅ computed — memoized, reactive157 readonly activeUsers = computed(() => this.users().filter(u => u.active));158 ```159160- **`effect()` side effects** — flag `effect()` used to derive or transform state (that's `computed()`'s job). `effect()` is for side effects only (logging, localStorage, external calls).161162- **`signal.asReadonly()`** — flag writable signals exposed publicly from services; expose `asReadonly()`:163 ```ts164 // ❌ Writable signal leaked — consumers can mutate165 readonly users = signal<User[]>([]);166167 // ✅ Expose read-only; mutate only through service methods168 private readonly _users = signal<User[]>([]);169 readonly users = this._users.asReadonly();170 ```171172- **`toSignal()` with `initialValue`** — flag `toSignal()` calls on Observables that may not emit immediately without an `initialValue` or `{ requireSync: true }` — results in `undefined` signal until first emission.173174- **`linkedSignal()` (v19+)** — flag complex `computed()` + `effect()` combinations that reset a signal when another changes; `linkedSignal()` is the cleaner pattern.175176### 🔁 RxJS177178**Rule of thumb: RxJS for async/events, Signals for state. Convert at component boundary with `toSignal()`.**179180- **Unsubscribed observables** — flag `.subscribe()` without cleanup. Acceptable patterns: `takeUntilDestroyed()`, `async` pipe, `toSignal()`. Flag `ngOnDestroy` + manual `Subscription` arrays in new code — use `takeUntilDestroyed(this.destroyRef)` instead:181 ```ts182 // ❌ Manual unsubscribe boilerplate183 private sub = new Subscription();184 ngOnInit() { this.sub.add(obs$.subscribe(...)); }185 ngOnDestroy() { this.sub.unsubscribe(); }186187 // ✅ takeUntilDestroyed188 obs$.pipe(takeUntilDestroyed()).subscribe(...);189 ```190191- **`switchMap` for cancellable requests** — flag `mergeMap` or `concatMap` on search/autocomplete streams where only the latest result matters.192193- **Error handling** — flag Observable chains missing `catchError` — unhandled errors complete the stream and break the UI.194195- **`BehaviorSubject` as state** — flag `BehaviorSubject` used for state that is: (a) local to a component, (b) synchronous, (c) not shared across features. Replace with `signal()`.196197- **Nested subscriptions** — flag `.subscribe()` inside another `.subscribe()` — use `switchMap`, `mergeMap`, or `combineLatest` instead.198199### 🗺️ Routing & Lazy Loading200201- **Lazy loading** — flag `loadComponent` / `loadChildren` not used for feature routes. Every feature route should be lazy-loaded:202 ```ts203 // ❌ Eager — entire module loaded upfront204 { path: 'users', component: UsersComponent }205206 // ✅ Lazy — loaded on demand207 { path: 'users', loadComponent: () => import('./users/users.component') }208 ```209210- **Functional guards** — flag class-based `CanActivate` / `CanDeactivate` guards in Angular 15+ projects; use functional guards:211 ```ts212 // ❌ Class-based guard (legacy)213 @Injectable()214 export class AuthGuard implements CanActivate { ... }215216 // ✅ Functional guard217 export const authGuard: CanActivateFn = (route, state) => {218 return inject(AuthService).isAuthenticated()219 ? true220 : inject(Router).createUrlTree(['/login']);221 };222 ```223224- **`input.fromRoute()` (v17.1+)** — flag components using `ActivatedRoute.snapshot.params` or `paramMap` subscriptions; prefer `input.fromRoute()` where route inputs are enabled.225226- **`withComponentInputBinding()`** — flag projects not enabling route component input binding in `provideRouter()` — required for `input.fromRoute()` to work.227228- **Route prefetching** — flag critical user flows without `prefetch` strategy on lazy routes.229230### 🌐 HttpClient & REST231232- **Services, not components** — flag `HttpClient` injected directly in a component; all HTTP calls belong in services.233234- **`httpResource()` (v19.2+)** — acceptable for component-level reactive data fetching. Flag it inside services (use `HttpClient` there).235236- **Error handling** — flag HTTP calls without `catchError` or error state handling in the UI.237238- **Typed HTTP responses** — flag `http.get('/api/users')` without a type parameter; always use `http.get<User[]>('/api/users')`.239240- **Interceptors** — flag class-based `HttpInterceptor` in Angular 15+ projects; use functional interceptors:241 ```ts242 // ✅ Functional interceptor243 export const authInterceptor: HttpInterceptorFn = (req, next) => {244 const token = inject(AuthService).getToken();245 return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));246 };247 ```248249- **Retry logic** — flag API calls that should retry on transient failures (network errors) without `retry()` or `retryWhen()`.250251### 🔐 Authentication (JWT / OAuth / Guards)252253- **Route guards** — flag protected routes missing `canActivate` with an auth guard.254- **Token storage** — flag JWT stored in `localStorage`; prefer `httpOnly` cookies or `sessionStorage` with XSS mitigations.255- **Token refresh** — flag implementations that don't handle `401` responses with token refresh logic in an interceptor.256- **`isAuthenticated` as signal** — flag auth state exposed as Observable where `signal()` + `toSignal()` would be cleaner for template consumption.257- **Role-based access** — flag guards checking only authentication, not authorization (roles/permissions) for admin or privileged routes.258- **OAuth `state` parameter** — flag OAuth redirect flows missing CSRF state validation.259260### 📦 NgRx261262- **Tiered state rule** — flag NgRx used for purely local component state; use `signal()` instead. NgRx is for: global state, cross-feature shared data, and complex side effects.263264- **NgRx Signal Store (v17+)** — flag class-based `@ngrx/store` for new features in modern Angular apps; prefer `signalStore()`:265 ```ts266 // ✅ NgRx Signal Store pattern267 export const UsersStore = signalStore(268 { providedIn: 'root' },269 withState(initialState),270 withComputed(({ users }) => ({271 activeUsers: computed(() => users().filter(u => u.active))272 })),273 withMethods((store, usersService = inject(UsersService)) => ({274 loadUsers: rxMethod<void>(275 pipe(276 switchMap(() => usersService.getAll()),277 tapResponse({278 next: users => patchState(store, { users }),279 error: console.error280 })281 )282 )283 }))284 );285 ```286287- **Effects** — flag side effects (HTTP calls, routing, notifications) placed directly in reducers or components; use NgRx Effects or `withMethods` + `rxMethod`.288- **Selectors** — flag repeated `store.select()` calls for the same data; extract to reusable selectors.289- **`patchState`** — flag direct state mutation attempts; always use `patchState()` in Signal Store methods.290291### 🎨 Angular Material292293- **Theming** — flag direct color values in component styles that should use Material theme tokens.294- **`MatFormFieldModule`** — flag form fields missing proper `appearance` attribute.295- **Accessibility** — flag `mat-icon` buttons missing `matTooltip` or `aria-label`.296- **Module vs standalone imports** — flag importing entire `MatButtonModule` where only `MatButton` directive is needed (standalone tree-shaking).297298### 🖥️ Angular Universal / SSR299300- **`isPlatformBrowser()`** — flag direct `window`, `document`, or `localStorage` access without platform check; these crash during SSR:301 ```ts302 // ❌ Crashes on server303 ngOnInit() { localStorage.setItem('key', 'value'); }304305 // ✅ Platform-safe306 ngOnInit() {307 if (isPlatformBrowser(this.platformId)) {308 localStorage.setItem('key', 'value');309 }310 }311 ```312313- **`afterNextRender()` / `afterRender()`** — flag `ngAfterViewInit` used for browser-only DOM operations in SSR apps; use `afterNextRender()` (runs only in browser).314315- **`TransferState`** — flag SSR apps making the same HTTP request on both server and client; use `TransferState` or `httpResource()` to cache server-fetched data.316317- **Incremental hydration (v19+)** — flag large, below-the-fold components not using `@defer` with hydration triggers.318319- **Event replay** — flag SSR apps on v18+ not enabling `withEventReplay()` in `provideClientHydration()` — user interactions before hydration are lost.320321### 🔵 GraphQL / Apollo Angular (if present)322323- **Typed queries** — flag Apollo `useQuery` / `gql` calls without generated TypeScript types; use `graphql-codegen`.324- **`watchQuery` vs `query`** — flag `query()` used where the UI needs live cache updates; use `watchQuery()`.325- **Loading/error states** — flag components using Apollo without handling `loading` and `error` states in the template.326- **`fetchPolicy`** — flag missing `fetchPolicy` on queries where stale data is a concern.327- **Mutations and cache** — flag mutations not updating the Apollo cache (`update` function or `refetchQueries`) when they modify list data.328329---330331## Anti-Patterns Quick Reference332333Flag these immediately when spotted:334335| Anti-pattern | Severity | Fix |336|---|---|---|337| `*ngIf` / `*ngFor` in new code (v17+) | Medium | Use `@if` / `@for` |338| `@for` without `track` | High | Add `track item.id` |339| `subscribe()` in component body | High | Use `toSignal()` or `async` pipe |340| Constructor injection | Low | Use `inject()` |341| `@Input()` / `@Output()` in new code (v17.1+) | Low | Use `input()` / `output()` |342| `BehaviorSubject` for local state | Medium | Use `signal()` |343| `effect()` for state derivation | High | Use `computed()` |344| Writable `signal` exposed from service | Medium | Use `.asReadonly()` |345| `NgModule` for new features (v17+) | Medium | Use standalone components |346| `CommonModule` in standalone imports | Low | Import specific directives |347| Direct `window`/`document` in SSR app | Critical | Use `isPlatformBrowser()` |348| HTTP call in component | Medium | Move to service |349| `[innerHTML]` without sanitizer | Critical | Use `DomSanitizer` or restructure |350| Missing `OnPush` | Medium | Add `ChangeDetectionStrategy.OnPush` |351| Nested `.subscribe()` | High | Use `switchMap`/`combineLatest` |352| Unsubscribed observable | High | Use `takeUntilDestroyed()` |353| Angular 19 < 19.2.18 | Critical | Update — XSS CVE |354| Angular 19 (EOL May 2026) | High | Migrate to Angular 20 |355356---357358## Unified Output Format359360Use the same format as `code-reviewer` (universal). Add an Angular context line:361362```363🔍 Environment: Angular v20.x | TypeScript 5.8 | RxJS 7.x364 NgRx Signal Store: v19.x | Apollo Angular: N/A365 Mode: Standalone | Zoneless: Developer Preview enabled366367## Code Review Summary368[... standard universal format ...]369370### 🅰️ Angular-Specific Issues371[Issues found by this overlay, using the same severity/format as universal]372```373374---375376## Behavior Rules (Angular-specific additions)377378- **Version first, always** — never apply version rules without detecting Angular version. Angular 19 EOL and XSS CVE are Critical-level findings.379- **Signals-first mindset for state, RxJS for streams** — don't flag RxJS in service layers; do flag RxJS `BehaviorSubject` replacing `signal()` in component state.380- **`OnPush` is always required** — no exceptions for new components regardless of version.381- **SSR is high-risk** — `window`/`document`/`localStorage` without platform guard is always Critical in SSR projects.382- **Don't refactor working NgRx to Signals** — if existing class-based NgRx works, flag as Low/suggestion only; don't gate a PR on it.383- **Delegate deep security audits** → `security-auditor` skill if available.