Promptheus/rules53 rule sets · CC0Promptheus hub ↗

Framework · Angular 22 · Standalone · Signals · Zoneless

Angular

Standalone components, signals and typed forms — modern Angular.

angulartypescriptsignalsrxjs

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You write Angular for a signal-first, zoneless, standalone codebase. "Good" means: no NgModules, no any, no leaked subscriptions, OnPush everywhere, signals for state, built-in control flow, and code that typechecks under strict and passes lint on the first run.

Stack

  • Angular 22.0.x (@angular/core, @angular/common, @angular/router, @angular/forms, @angular/platform-browser). Standalone + signals + zoneless is the baseline.
  • TypeScript 6.0.x (>=6.0.0 <6.1.0). 5.9 and below are unsupported by v22. strict: true plus noUncheckedIndexedAccess, noImplicitOverride, exactOptionalPropertyTypes.
  • Node.js 24 LTS (supported range ^22.22.3 || ^24.15.0 || ^26.0.0). Node 20 is EOL — do not target it.
  • RxJS 7.8.x (v22 peer range ^6.5.3 || ^7.4.0; ship the latest 7.8.x — v22 does not support RxJS 8 yet). Used for streams/events only, never for component state.
  • Angular CLI 22 with the @angular/build (esbuild/Vite) application builder. No @angular-devkit/build-angular webpack builder for new apps.
  • zone.js is not a dependency in new apps — zoneless change detection is stable and the default. Only add zone.js (0.15.x) if you must interop with a legacy zone-dependent lib.
  • Signal Forms (@angular/forms/signals) — stable in v22 — for new forms. Typed non-nullable Reactive Forms (@angular/forms) remain valid for existing/complex flows.
  • Vitest via @angular/build:unit-test is the default test runner. Karma/Jasmine are removed for new projects. Playwright for e2e.
  • Lint/format: angular-eslint + typescript-eslint (flat config, strict-type-checked) + Prettier.

Project conventions

  • Standalone only. No NgModule, no declarations, no SharedModule. Composition is via a component/directive/pipe imports array.
  • Folder layout: feature-first. src/app/<feature>/ holds that feature's components, services, routes (<feature>.routes.ts), and models. Cross-cutting singletons in src/app/core/, reusable presentational pieces in src/app/shared/.
  • File naming (v20+ style guide): drop the type suffix. user-profile.ts exporting class UserProfile, auth.ts exporting Auth service, <feature>.routes.ts for route arrays. Templates/styles as sibling .html/.css files; inline only for <15-line templates.
  • Imports: always use the @angular/* public entry points. Configure a @app/* path alias in tsconfig for app-internal absolute imports; no deep ../../../ chains.
  • Bootstrap: bootstrapApplication(App, appConfig) in main.ts; providers live in app.config.ts via ApplicationConfig. No platformBrowserDynamic().bootstrapModule.
  • Run npx prettier --write and ng lint before every commit. ng lint must be clean — do not disable rules inline to pass.

Components and templates

  • Every component is standalone with OnPush. v22 makes OnPush the default, so omit changeDetection; never set ChangeDetectionStrategy.Default.

  • Signal-based I/O only. Use input() / input.required<T>(), output(), and model() — never the @Input() / @Output() decorators.

    readonly userId = input.required<string>();
    readonly saved = output<User>();
    readonly value = model<string>('');   // two-way: [(value)]
    
  • Signal queries: viewChild(), viewChildren(), contentChild(), contentChildren() — not the @ViewChild/@ContentChild decorators.

  • Built-in control flow only. Use @if / @else, @for (with mandatory track), @switch. Never *ngIf, *ngFor, *ngSwitch, or NgIf/NgForOf imports.

    @for (item of items(); track item.id) {
      <app-row [item]="item" />
    } @empty {
      <p>No items.</p>
    }
    

    track must be a stable identity (id), never $index for keyed lists.

  • Use @let for template-local derived values and @defer (on idle/on viewport/on interaction) for lazy, hydration-friendly rendering.

  • Host bindings go in the host object of the decorator, not @HostBinding/@HostListener.

  • Prefer [class.x]/[style.x] bindings over ngClass/ngStyle. Import only the pipes you actually use (AsyncPipe, DatePipe, etc.), not CommonModule wholesale.

  • Bind observables with the async pipe or convert with toSignal — never call .subscribe() in a backing field to push into a mutable property.

State and reactivity (Signals)

  • Component and shared state is signals: signal() for writable, computed() for derived, linkedSignal() for derived-but-locally-writable state (e.g. a selection that resets when its source list changes).
  • Update immutably: count.update(c => c + 1) / state.set(next). Never mutate the object inside a signal in place.
  • computed for derivation; effect only for side effects (DOM you can't template, logging, localStorage, syncing to a non-Angular lib). Never use an effect to copy one signal into another — that's a computed/linkedSignal. Effects run in an injection context and auto-clean on destroy.
  • Use untracked() to read a signal inside a computed/effect without creating a dependency.
  • Keep signals readonly when exposed; expose writes via methods or .asReadonly(). A store service holds #state = signal(...) privately and exposes computed selectors.

Data fetching and HTTP

  • Provide the client once: provideHttpClient(withFetch(), withInterceptors([authInterceptor])). withFetch() is required for the modern fetch backend and SSR streaming.

  • Interceptors are functions (HttpInterceptorFn), registered via withInterceptors([...]). No class-based HTTP_INTERCEPTORS multi-providers.

  • Reads → httpResource (stable in v22) for declarative, signal-driven GETs that re-fetch when inputs change; it exposes value(), status(), isLoading(), error() as signals.

    readonly userId = input.required<string>();
    readonly user = httpResource<User>(() => `/api/users/${this.userId()}`);
    // template: @if (user.isLoading()) { … } @else { {{ user.value()?.name }} }
    
  • Writes / commands → typed HttpClient: inject(HttpClient).post<Order>('/api/orders', body). Always supply the response generic; never leave it Object.

  • Use resource() / rxResource() for non-HTTP async or when you need an RxJS loader. For search-as-you-type, the debounced() primitive (@angular/core) keeps debouncing inside the signal graph — but it is experimental (developer preview) in v22 and may change, so adopt it only behind that caveat. The stable fallback is debounceTime on a toObservable(query) stream feeding rxResource.

  • Type every payload with an interface/type. No any on responses — decode/narrow unknown external data at the boundary.

RxJS interop

  • Signals for state, RxJS for streams (websockets, DOM events, complex time-based orchestration). Do not model plain component state as a BehaviorSubject.
  • Bridge with toSignal(obs$, { initialValue }) and toObservable(sig). toSignal unsubscribes automatically on destroy.
  • If you must .subscribe() manually, pipe takeUntilDestroyed() (call it in an injection context, or pass a DestroyRef). A bare .subscribe() with no teardown is a leak and is not allowed.
  • Prefer declarative composition (switchMap, combineLatest) over nested subscribes. One subscription per stream, torn down deterministically.

Forms

  • New forms → Signal Forms (@angular/forms/signals). The model is a writable signal with non-nullable initial values ('', 0, [] — never null). Bind inputs with the [formField] directive.

    import { form, FormField, submit, required, email } from '@angular/forms/signals';
    
    protected readonly model = signal({ email: '', password: '' });
    protected readonly loginForm = form(this.model, (path) => {
      required(path.email, { message: 'Email required' });
      email(path.email);
      required(path.password);
    });
    
    protected send() {
      submit(this.loginForm, async () => this.auth.login(this.model()));
    }
    

    Template: <input type="email" [formField]="loginForm.email" />; read state via loginForm.email().errors(), ().touched(), ().valid(), and loginForm().invalid(). Use disabled/readonly/hidden/applyWhen/applyEach for conditional logic and validate/validateAsync for custom rules.

  • Existing/complex forms → typed non-nullable Reactive Forms. Build with inject(NonNullableFormBuilder) so controls are FormControl<T> (never T | null). Never new FormBuilder() untyped, never nullable controls, never [ngModel] template-driven forms for anything non-trivial.

  • Validation is typed and declarative; surface errors from the control/field state, not by reaching into the DOM.

Routing

  • provideRouter(routes, withComponentInputBinding(), withViewTransitions()) in app.config.ts. Route params/query/data bind straight to component input()s via withComponentInputBinding().
  • Lazy-load with loadComponent (single component) and loadChildren: () => import('./x.routes') (feature route arrays). No eagerly imported feature trees.
  • Guards and resolvers are functions: CanActivateFn, CanMatchFn, ResolveFn, using inject() inside. No class-based CanActivate guards.
  • Prefer CanMatch over CanActivate for auth so unauthorized routes don't even match (and their lazy bundles never load).

Dependency injection

  • inject() everywhere — in fields, functions, guards, interceptors, and factories. Constructor-parameter injection only when a base class forces it.
  • Tree-shakable singletons via @Injectable({ providedIn: 'root' }). Feature/route-scoped services via the route's providers.
  • Config/values via typed InjectionToken<T> with a factory; never inject by string. Use inject(TOKEN, { optional: true }) for optional deps.

Testing

  • Vitest (ng test, builder @angular/build:unit-test, jsdom env). Co-locate *.spec.ts next to source.
  • TestBed.configureTestingModule({ imports: [Component] }) for standalone components — components go in imports, not declarations. Override providers with { provide, useValue }.
  • Test behavior through signals and the rendered DOM: set inputs via fixture.componentRef.setInput('userId', 'x'), call fixture.detectChanges(), assert on componentInstance.someSignal() and on DOM queried through CDK ComponentHarness rather than brittle CSS selectors.
  • Mock HTTP with provideHttpClientTesting() + HttpTestingController; assert requests and flush() typed responses. For httpResource, drive the signal input and flush the expected URL.
  • Test signal logic (computed/linkedSignal) as plain functions where possible — they're synchronous and need no TestBed. Use fake timers for debounced/async flows. e2e critical paths in Playwright.
  • No test may pass by disabling change detection or asserting on any. Cover: guard allow/deny, form validation states, error branches of resources.

Security

  • Never bypass sanitization. Avoid bypassSecurityTrustHtml/...TrustResourceUrl; if unavoidable, sanitize the source first and document why. Angular's default interpolation/binding sanitization must remain intact — no innerHTML with untrusted data.
  • Set a strict Content-Security-Policy. Angular supports CSP nonces via the CSP_NONCE token; do not enable unsafe-inline.
  • Enable Angular's built-in XSRF: provideHttpClient(withXsrfConfiguration(...)) and ensure the backend sets the cookie/verifies the header.
  • Never interpolate user input into a template, URL, or [href]/[src] without validation; javascript: URLs are stripped by Angular — don't re-introduce them via a bypass call.
  • Keep secrets/tokens out of the client bundle and out of localStorage where feasible (prefer HttpOnly cookies). Environment files are public — treat anything shipped to the browser as exposed.
  • Run npm audit, keep Angular current, pin dependency ranges, and review transitive updates.

Do

  • Bootstrap standalone with bootstrapApplication + ApplicationConfig; keep providers in app.config.ts.
  • Model all state as signal/computed/linkedSignal; expose read-only signals from services.
  • Use @if/@for(+track)/@switch, @let, and @defer in templates.
  • Use input()/output()/model()/viewChild() signal APIs and inject() for DI.
  • Fetch reads with httpResource/resource; mutate with typed HttpClient; write interceptors and guards as functions.
  • Lazy-load routes with loadComponent/loadChildren and bind params with withComponentInputBinding().
  • Keep OnPush (the v22 default), non-nullable typed forms, and clean teardown (toSignal/async pipe/takeUntilDestroyed).

Avoid

  • NgModule, declarations, SharedModule, blanket CommonModule imports → standalone components with explicit imports.
  • *ngIf / *ngFor / *ngSwitch / ngClass / ngStyle@if / @for track / @switch and [class.x]/[style.x].
  • @Input()/@Output()/@ViewChild() decoratorsinput()/output()/model()/viewChild().
  • ChangeDetectionStrategy.Default → rely on the OnPush default; never opt out.
  • .subscribe() without takeUntilDestroyed(), and BehaviorSubject-as-state → toSignal/signals.
  • effect() to sync signalscomputed/linkedSignal.
  • Untyped HttpClient calls, class-based interceptors/guards, nullable FormBuilder controls, template-driven ngModel forms → typed generics, functional HttpInterceptorFn/CanMatchFn, NonNullableFormBuilder or Signal Forms.
  • any (and as any casts) → real interfaces, generics, unknown + narrowing.
  • platformBrowserDynamic().bootstrapModule, the webpack build-angular builder, zone.js in new appsbootstrapApplication, @angular/build, zoneless.

When you code

  • Make small, focused diffs. Touch one feature/concern per change; don't opportunistically rewrite unrelated files or "modernize" code you weren't asked to.
  • After any change, run ng lint, tsc --noEmit (or ng build), and ng test for the affected area, and report the results. Do not hand back code that fails typecheck or lint.
  • When adding a component/service, wire it end to end (providers, route, imports) — no dangling, unreferenced files.
  • Match the existing pattern in the file; if the repo still uses an older idiom (e.g. decorators or *ngFor), follow the surrounding style unless the task is explicitly to migrate.
  • Ask before: introducing a new dependency, changing global providers/app.config.ts, switching the forms or state strategy, altering the build/test toolchain, or migrating away from an established pattern. Prefer the framework primitive over adding a library.
  • If a requested API is deprecated or removed in v22, say so and implement the current equivalent instead of the outdated one.

Drop it in your repo

Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on Angular 22 · Standalone · Signals · Zoneless.

Back to top ↑