The first time I let Claude Code touch a clean Angular 19 codebase without a CLAUDE.md at the repo root, the assistant scaffolded a feature using @NgModule, *ngIf, constructor @Input() decorators, and a BehaviorSubject field for state. Tests passed. Compiler was green. It was the wrong Angular.
The training data for every frontier model is a decade of Angular evolution stacked on top of itself: NgModule apps, the standalone preview, signal-based authoring, the new control flow, the zoneless story. Without explicit guidance, an AI picks whichever pattern was statistically loudest in its training set. A CLAUDE.md file at the repo root collapses that probability cloud into one shape — your shape.
Here are the seven rules I port across every Angular 17+ project I touch in 2026. Full template here: CLAUDE.md Angular Edition gist.
1. Standalone everything — no new NgModule
Components, directives, pipes are standalone. NEVER create a new @NgModule.
Bootstrap with bootstrapApplication(AppComponent, { providers: [...] }).
If a task touches a legacy NgModule-registered component, migrate it
to standalone in the same PR.
Why this matters for Angular: the model defaults to whatever 80% of its training data shows, which is the NgModule era. Naming the migration policy ("touch it, migrate it") prevents a long tail of mixed-paradigm PRs. Lazy routes use loadChildren: () => import('./feature/feature.routes').then(m => m.FEATURE_ROUTES) — never the legacy loadModule pattern.
2. New control flow — @if, @for, @switch
Templates use @if, @for (with `track`), @switch, and @empty.
NEVER mix *ngIf / *ngFor / *ngSwitch with the new syntax in one template.
Why this matters for Angular: the new control flow is compile-time optimized, narrows types correctly inside @if (user(); as user), and produces smaller bundles by dropping NgIf / NgForOf from CommonModule. @for requires a track expression at compile time — without it Angular throws, which is the right default. AI assistants that haven't seen track enough will fall back to *ngFor="let u of users" and silently regress your performance.
3. Signals first, BehaviorSubject last
Component state is signal(). Derived state is computed(). Side effects
are effect(). Inputs are input() / input.required(). Outputs are output().
Two-way binding is model(). NEVER use a private BehaviorSubject + asObservable()
getter for component-local state.
Why this matters for Angular: signals are the language now. The BehaviorSubject field with a public asObservable() getter was the workaround for the years before Angular had primitive reactivity. Without this rule the model will generate that pattern by default because it dominates the training data. input.required<string>() also catches the missing-input bug at compile time, which @Input() name!: string only catches at runtime when the parent forgets to bind it.
4. Smart/dumb component separation
Dumb components: take input(), emit output(), zero injected services,
ChangeDetectionStrategy.OnPush, droppable into Storybook with no DI mocks.
Smart components: inject services with inject(), orchestrate state, pass
data down to dumb components. Folder convention: pages/*.page.ts (smart)
and components/*.component.ts (dumb).
Why this matters for Angular: AI assistants love to "just inject the service here" because it minimizes lines added. That puts HTTP calls into a dumb card component and makes it untestable in isolation. The folder naming (*.page.ts vs *.component.ts) is a contract that's enforced by code review and visible in every import statement.
5. inject() and providedIn: 'root'
Use inject(UsersService) in constructors and factory functions.
@Injectable({ providedIn: 'root' }) for singleton services so they
tree-shake. NEVER list singletons in AppComponent's providers array.
NEVER `new UserService()` in business code. Use takeUntilDestroyed(inject(DestroyRef))
to clean up subscriptions tied to component lifecycle.
Why this matters for Angular: providedIn: 'root' is tree-shakable; listing services in AppComponent.providers defeats it and quietly bloats the initial bundle. takeUntilDestroyed() replaces the old private destroy$ = new Subject<void>(); ngOnDestroy() { this.destroy$.next() } boilerplate that an AI will keep generating because it dominates Stack Overflow answers from 2019–2022.
6. Reactive forms with NonNullableFormBuilder
Forms are reactive and typed via NonNullableFormBuilder.
NEVER use template-driven [(ngModel)] forms beyond a 5-line prototype.
NEVER use the legacy FormBuilder — its types include null for every field.
Why this matters for Angular: typed forms are the difference between form.value.email: string (with NonNullableFormBuilder) and form.value.email: string | null | undefined (with FormBuilder). The latter forces ! non-null assertions across every form handler downstream, which compounds into a typing mess. The AI will pick the legacy FormBuilder because it's the most-documented variant on the web — explicit in CLAUDE.md, explicit in code review.
7. Accessibility verified by axe-core in CI
Every interactive element has a discernible name (aria-label or visible text).
Buttons are <button type="button">, never <div (click)>.
Form labels are <label for> + <input id>; placeholder is NOT a label.
axe-core runs in e2e CI and fails the build on critical violations.
Color contrast checked at design time, verified in CI.
Why this matters for Angular: AI assistants generate <div (click)="onClick()"> constantly because it works in dev, looks fine, and passes type checking. It fails for keyboard users (divs aren't focusable), for screen reader users (not announced as actionable), and silently lowers your accessibility score until someone runs Lighthouse. Putting axe-core in the e2e pipeline means a regression breaks the build, not Q4 OKRs.
Drop the Angular CLAUDE.md template at the root of your Angular 17+ project, commit it, and watch the AI's suggestions converge on the codebase you actually want. The full Rules Pack covering 27+ stacks (Next.js, Vue 3, Svelte, FastAPI, Django, Go, Rust, NestJS, Angular, and more) lives at oliviacraftlat.gumroad.com/l/skdgt.
The signal here isn't "AI is bad at Angular." It's that AI is excellent at Angular when you tell it which Angular you mean. The rule is the configuration; without it, the assistant guesses, and statistical guesses average to 2018.
Top comments (0)