| name | angular |
| description | [Applies to: **/*.{ts,html}] Provides definitive guidelines for modern Angular development (2025), focusing on best practices for code structure, component architecture, state management with signals, performance, and type safety. |
| source | cursor_mdc |
Angular Best Practices
This guide outlines the definitive best practices for Angular development, ensuring consistency, maintainability, and optimal performance. Adhere to these rules strictly.
1. Code Organization and Structure
1.1 One Concept Per File (SRP)
Rule: Each file must contain a single Angular concept (e.g., one component, one directive, one pipe, one service, one interface).
Explanation: This enforces the Single Responsibility Principle (SRP), improving modularity, readability, and testability. Exceptions are rare and only for tightly coupled, non-exported elements.
❌ BAD:
export interface User { id: string; name: string; }
@Injectable({ providedIn: 'root' })
export class UserService {
}
✅ GOOD:
export interface User { id: string; name: string; }
@Injectable({ providedIn: 'root' })
export class UserService {
}
1.2 Consistent Naming Conventions
Rule: Follow kebab-case for file names (feature-name.type.ts) and PascalCase for class names. Use standard type suffixes.
Explanation: Consistent naming makes files easy to locate and understand at a glance.
❌ BAD:
export class UserListComp { }
export class AuthenticationService { }
✅ GOOD:
export class UserListComponent { }
export class AuthService { }
2. Component Architecture
2.1 Standalone Components by Default
Rule: Always create standalone components, directives, and pipes. Avoid NgModules for new features unless strictly necessary for legacy integration or specific library patterns.
Explanation: Standalone components simplify the Angular mental model, reduce boilerplate, and improve tree-shaking.
❌ BAD:
@NgModule({
declarations: [MyComponent],
imports: [CommonModule],
})
export class AppModule { }
@Component({
selector: 'app-my',
template: `<p>My Component</p>`,
styleUrls: ['./my.component.css'],
})
export class MyComponent { }
✅ GOOD:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-my',
standalone: true,
imports: [CommonModule],
template: `<p>My Component</p>`,
styleUrls: ['./my.component.css'],
})
export class MyComponent { }
2.2 Prefer inject for Dependency Injection
Rule: Use the inject function for all dependency injection, especially in field initializers. Avoid constructor injection unless required for specific lifecycle hooks or inheritance patterns.
Explanation: inject offers greater flexibility, better tree-shaking, and allows injection outside of constructors.
❌ BAD:
import { MyService } from './my.service';
@Component({...})
export class MyComponent {
private myService: MyService;
constructor(myService: MyService) {
this.myService = myService;
}
}
✅ GOOD:
import { inject } from '@angular/core';
import { MyService } from './my.service';
@Component({...})
export class MyComponent {
private readonly myService = inject(MyService);
}
3. State Management
3.1 Prioritize Signals for Component State
Rule: Use Angular Signals for all local component state and simple derived state. Reserve RxJS for complex asynchronous data streams, side effects, and inter-component communication.
Explanation: Signals provide a reactive, performant, and simpler way to manage state, optimizing change detection automatically.
❌ BAD:
@Component({...})
export class MyComponent {
count = 0;
items: string[] = [];
addItem(item: string) {
this.items.push(item);
}
}
✅ GOOD:
import { Component, signal, computed } from '@angular/core';
@Component({...})
export class MyComponent {
readonly count = signal(0);
readonly items = signal<string[]>([]);
readonly hasItems = computed(() => this.items().length > 0);
increment() {
this.count.update(value => value + 1);
}
addItem(item: string) {
this.items.update(currentItems => [...currentItems, item]);
}
}
3.2 RxJS for Asynchronous Streams and Effects
Rule: Leverage RxJS for handling HTTP requests, complex event streams, and orchestrating side effects. Always manage subscriptions to prevent memory leaks.
Explanation: RxJS is powerful for reactive programming with asynchronous data. Use toSignal or takeUntilDestroyed for automatic unsubscription.
❌ BAD:
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users = users;
});
}
ngOnDestroy() {
}
✅ GOOD:
import { Component, inject, DestroyRef } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { UserService } from './user.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { tap } from 'rxjs';
@Component({...})
export class MyComponent {
private readonly userService = inject(UserService);
private readonly destroyRef = inject(DestroyRef);
readonly users = toSignal(this.userService.getUsers(), { initialValue: [] });
constructor() {
this.userService.someAction$
.pipe(
tap(action => console.log('Action:', action)),
takeUntilDestroyed(this.destroyRef)
)
.subscribe();
}
}
4. Performance Considerations
4.1 Use Built-in Control Flow
Rule: Discontinue the use of structural directives (*ngIf, *ngFor, *ngSwitch). Always use Angular's new built-in control flow syntax.
Explanation: The new control flow (@if, @for, @switch) offers improved performance, better type checking, and a more intuitive syntax.
❌ BAD:
<div *ngIf="isLoading">Loading...</div>
<li *ngFor="let item of items; let i = index">{{ item.name }}</li>
✅ GOOD:
@if (isLoading) {
<div>Loading...</div>
} @else {
<div>Data loaded!</div>
}
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<p>No items found.</p>
}
4.2 Always Use track Key in @for
Rule: Always provide a track key in @for loops. Use a unique, primitive property of the object (e.g., item.id). If iterating over primitives, use the value directly.
Explanation: The track key helps Angular optimize rendering by identifying unique items, preventing unnecessary DOM re-renders and improving performance.
❌ BAD:
@for (user of users) {
<li>{{ user.name }}</li>
}
✅ GOOD:
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
@for (name of names; track name) {
<li>{{ name }}</li>
}
5. Type Safety
5.1 Define Interfaces for Data Structures
Rule: Always define TypeScript interfaces or types for all data structures, especially those received from APIs or used in models.
Explanation: Interfaces provide strong type checking, improve code readability, and catch errors early.
❌ BAD:
fetchUsers().subscribe((data: any) => {
console.log(data.users[0].firstName);
});
✅ GOOD:
export interface User {
id: string;
firstName: string;
lastName: string;
email: string;
}
fetchUsers().subscribe((data: User[]) => {
console.log(data[0].firstName);
});
5.2 Default to readonly Properties
Rule: Declare component properties and injected services as readonly where their values are not intended to be reassigned after initialization.
Explanation: readonly encourages immutability, prevents accidental reassignments, and makes state flow more predictable.
❌ BAD:
@Component({...})
export class MyComponent {
title = 'My App';
private myService = inject(MyService);
}
✅ GOOD:
@Component({...})
export class MyComponent {
readonly title = 'My App';
private readonly myService = inject(MyService);
}
6. Accessibility (A11y)
6.1 Leverage Angular Aria and CDK
Rule: Integrate Angular Aria and the Component Dev Kit (CDK) for building accessible UI components. Always provide appropriate ARIA attributes and semantic HTML.
Explanation: Angular's built-in tools help ensure your applications are usable by everyone, including those with disabilities.
❌ BAD:
<div (click)="doSomething()">Click Me</div>
✅ GOOD:
<button type="button" (click)="doSomething()" aria-label="Perform action">Click Me</button>
<button cdkMenuTrigger>Open Menu</button>
<div cdkMenu>
<button cdkMenuItem>Item 1</button>
</div>