Angular Cursor Rules: AI Coding Guardrails That Work
Copy-paste Angular Cursor rules for signals, standalone components, inject(), and OnPush — plus how to share one rule set with Claude Code and Copilot.
Angular Cursor rules are project-level instruction files — .cursor/rules/*.mdc — that tell Cursor which Angular patterns your codebase actually uses, so it stops generating NgModule boilerplate and constructor injection and starts writing signals, standalone components, and inject(). Adding them is the single highest-leverage change you can make to your Angular AI coding setup, because the default output of every model is Angular as it looked around 2018.
This guide gives you a complete, copyable rule set covering signals, standalone components, OnPush change detection, the built-in template control flow, RxJS discipline, and testing — plus the exact steps to run the same rules in Claude Code and GitHub Copilot so everyone on the team gets identical guardrails regardless of which tool they open.
Why AI writes 2018-era Angular by default
Angular changed more in its last several major versions than most frameworks change in a decade. Standalone components killed the NgModule ceremony. Signals replaced a large chunk of RxJS-for-everything state management. @if and @for replaced structural directives. inject() replaced constructor injection as the idiomatic way to get dependencies. Zoneless change detection is coming for Zone.js.
Training data has not caught up. There are years of tutorials, Stack Overflow answers, and open-source code teaching the old patterns, and a comparatively thin slice teaching the new ones. So when you ask an AI tool for an Angular component with no other context, you reliably get:
- An NgModule with a
declarationsarray — in a codebase that has zero modules constructor(private http: HttpClient)instead ofinject(HttpClient)@Input()and@Output()decorators instead ofinput()andoutput()*ngIfand*ngForinstead of@ifand@for.subscribe()inngOnInitwith cleanup inngOnDestroy— or no cleanup at all- Default change detection on every component
Almost none of this fails to compile. Deprecated-but-supported patterns are the worst kind of AI output: they work, they pass CI, and they quietly split your codebase into two eras. The fix isn't better prompting — you'd have to repeat yourself in every request. The fix is rules that load automatically. If you haven't set up Cursor rules before, the complete guide to Cursor rules covers the file format, scoping, and how rules attach to requests. This post is the Angular-specific payload.
Angular Cursor rules for signals, standalone components, and inject()
Create .cursor/rules/angular-core.mdc with the conventions that define modern Angular. This is the file that does the heavy lifting:
---
description: Modern Angular conventions — signals, standalone, inject(), OnPush
globs: ["src/**/*.ts", "src/**/*.html"]
alwaysApply: true
---
## Components
- All components, directives, and pipes are standalone. NEVER create or modify NgModules.
- Set changeDetection: ChangeDetectionStrategy.OnPush on every component.
- Keep components thin: rendering and user interaction only. Business logic
lives in services or plain functions.
## Dependency injection
- Inject dependencies with inject() in field initializers:
private readonly userService = inject(UserService);
- NEVER use constructor parameter injection.
## State and reactivity
- Local component state is a signal(): readonly count = signal(0);
- Derived state is a computed(). Never recompute derived values imperatively.
- Component inputs use input() and input.required(), not @Input().
- Outputs use output(), not @Output() with EventEmitter.
- Two-way bindings use model().
- View queries use signal-based viewChild() / contentChild(), not decorator queries.
- effect() is a last resort for syncing signals with imperative APIs
(charts, maps, focus). NEVER use effect() to write one signal from
another — that is what computed() is for.
The effect() rule deserves special attention. These Cursor rules for Angular signals exist because models love writing effect() blocks that set other signals — recreating exactly the tangled update graphs signals were designed to eliminate. Stating explicitly that computed() owns derivation and effect() is an escape hatch removes the most common signal antipattern from generated code.
The OnPush rule works because signals make it safe. When every piece of template state is a signal read, change detection knows precisely what changed, and OnPush stops being the scary optimization it was in the Zone.js-everywhere days. Enforcing both together — signals for state, OnPush on every component — also positions the codebase for zoneless change detection later.
Template and RxJS conventions
Templates are where outdated AI output is most visible, and RxJS is where it does the most silent damage. Add .cursor/rules/angular-templates.mdc:
---
description: Angular template control flow and RxJS conventions
globs: ["src/**/*.ts", "src/**/*.html"]
alwaysApply: true
---
## Templates
- Use built-in control flow: @if, @for, @switch. NEVER use *ngIf, *ngFor, *ngSwitch.
- Every @for has a track expression (track item.id; track $index only for
static lists).
- Pair @for with @empty for empty states.
- Read signals directly in templates: {{ user().name }}.
- No function calls in templates except signal reads and pipes. Anything with
logic becomes a computed().
- Use @defer for heavy components below the fold.
## RxJS
- Convert HTTP responses to signals at the component boundary with toSignal()
(from @angular/core/rxjs-interop).
- If a manual subscribe() is unavoidable, pipe takeUntilDestroyed() in an
injection context.
- NEVER store Subscription fields and clean them up in ngOnDestroy.
- Never nest subscribe() calls. Compose with switchMap, concatMap, or exhaustMap.
- Rule of thumb: signals for state, RxJS for events over time (HTTP,
websockets, debounced input).
The control-flow rule has a built-in enforcement bonus: @for won't compile without a track expression, so pushing the AI onto the new syntax eliminates the classic un-keyed *ngFor performance bug by construction. The RxJS rules are about leaks — a .subscribe() without teardown is the kind of bug that passes every review and shows up weeks later as a memory-profile mystery.
Testing rules for specs that pass review
Left alone, AI-generated Angular specs converge on expect(component).toBeTruthy() and little else. Worse, models trained on module-era testing generate declarations arrays that don't work with standalone components, and set inputs by assigning fields — which silently does nothing under OnPush and doesn't compile at all with signal inputs. Add .cursor/rules/angular-testing.mdc:
---
description: Angular testing conventions
globs: ["src/**/*.spec.ts"]
alwaysApply: false
---
## Testing
- Standalone components go in imports, never declarations:
TestBed.configureTestingModule({ imports: [UserCardComponent] })
- Set inputs with fixture.componentRef.setInput('user', mockUser) — never
assign component fields directly. Required for signal inputs and OnPush.
- Test HTTP with provideHttpClient() + provideHttpClientTesting() and
HttpTestingController. Never hit real endpoints.
- Test behavior through the DOM: render, interact, assert on output. Do not
assert on private fields or spy on internal methods.
- Use fakeAsync + tick for timers and debounce logic. No real setTimeout waits.
- A spec that only checks "should create" is not a test. Every spec asserts
observable behavior.
The setInput rule alone will save you review cycles — it's the single most common way AI-written Angular specs go wrong, and the failure mode is a test that passes while testing nothing, because the assertion runs against stale change detection. For more on writing rules that models actually follow — short, imperative, a correct example next to each prohibition — see AI coding rules best practices.
Cross-tool setup: Cursor, Claude Code, and Copilot
Rules only work if they're loaded in whatever tool a teammate happens to open. Here's the setup across the big three:
-
Cursor. Save the blocks above under
.cursor/rules/as.mdcfiles. The frontmatter controls attachment:alwaysApply: truefor the core conventions, glob-scoped for the testing rules so they only load when spec files are in play. -
Claude Code. Put a condensed version of the same conventions in
CLAUDE.mdat the repo root — Claude Code reads it at the start of every session. For the full rule set with examples, a skill keepsCLAUDE.mdlean:
.claude/skills/angular-conventions/
SKILL.md # the full rule set, loaded when Angular work comes up
-
GitHub Copilot. Copilot instructions for Angular live in
.github/copilot-instructions.md— plain markdown, no frontmatter, applied to Copilot chat and agent requests across the repo. Path-scoped variants go in.github/instructions/*.instructions.mdwith anapplyToglob, which maps neatly onto the testing rules. -
Keep all three in sync. This is where hand-copying breaks down: three files, three formats, and every rule tweak replicated three times — multiplied by every Angular repo you own. A registry solves this. Publish the rule set once as a skill on localskills, then install it into each tool's native format in one command:
localskills install your-org/angular-rules --target cursor claude windsurf
The CLI writes .cursor/rules/*.mdc for Cursor, .claude/skills/ for Claude Code, .windsurf/rules/ for Windsurf, and so on — one canonical source, versioned with rollback, private to your org if you want it to be. When the rules change, teammates install the new version instead of diffing markdown by hand. The same drift problem exists in every stack — the React and Next.js rule set is the equivalent playbook if your org runs both frameworks.
The complete Angular Cursor rules file
Prefer a single file? Here's the consolidated version. Save it as .cursor/rules/angular.mdc and adapt the details — the strongest rules encode decisions your team actually made, not generic advice.
---
description: Angular conventions — signals, standalone, OnPush, control flow, testing
globs: ["src/**/*.ts", "src/**/*.html"]
alwaysApply: true
---
# Angular conventions
## Architecture
- Standalone components, directives, and pipes only. NEVER create NgModules.
- changeDetection: ChangeDetectionStrategy.OnPush on every component.
- inject() in field initializers; never constructor parameter injection.
- Components handle rendering and interaction; logic lives in services.
## Reactivity
- signal() for local state, computed() for derived state.
- input() / input.required() / output() / model() — never decorator-based APIs.
- viewChild() / contentChild() signal queries, not decorator queries.
- effect() only to sync signals with imperative APIs; never to derive state.
## Templates
- @if / @for / @switch only; never *ngIf / *ngFor / *ngSwitch.
- @for always has track; pair with @empty where an empty state makes sense.
- Read signals directly in the template; computed() for anything with logic.
- @defer heavy below-the-fold components.
## RxJS
- toSignal() at the component boundary; takeUntilDestroyed() for manual
subscriptions.
- No Subscription fields, no ngOnDestroy cleanup, no nested subscribe().
- Signals for state; RxJS for events over time.
## Testing
- TestBed imports (never declarations); fixture.componentRef.setInput() for inputs.
- provideHttpClientTesting() + HttpTestingController for HTTP.
- Assert on rendered DOM and outputs, not private internals.
- fakeAsync + tick for time; no "should create"-only specs.
Treat the file as living documentation. When review catches the AI writing something your team wouldn't, that's a missing rule — add it, and the mistake stops recurring. When Angular ships its next reactivity primitive, you update one file instead of re-teaching every developer's tab-complete.
One rule set, every tool, every repo. Create a free localskills account to publish your Angular rules once and install them everywhere your team codes.
npm install -g @localskills/cli