Skip to main content
Best Practices

Best Practices Overview

Estimated Time: 2 hours | Difficulty: All Levels | Prerequisites: Complete Angular Course
This guide consolidates Angular best practices, coding standards, and architectural patterns to help you write maintainable, scalable, and performant applications.

Project Structure

src/
├── app/
│   ├── core/                    # Singleton services, guards, interceptors
│   │   ├── guards/
│   │   ├── interceptors/
│   │   ├── services/
│   │   └── core.ts
│   │
│   ├── shared/                  # Shared components, directives, pipes
│   │   ├── components/
│   │   ├── directives/
│   │   ├── pipes/
│   │   └── shared.ts
│   │
│   ├── features/                # Feature modules/routes
│   │   ├── products/
│   │   │   ├── components/
│   │   │   ├── services/
│   │   │   ├── models/
│   │   │   ├── products.routes.ts
│   │   │   └── products.component.ts
│   │   │
│   │   └── users/
│   │       └── ...
│   │
│   ├── layouts/                 # Layout components
│   │   ├── main-layout/
│   │   └── auth-layout/
│   │
│   ├── app.component.ts
│   ├── app.config.ts
│   └── app.routes.ts

├── environments/
├── assets/
└── styles/

Component Best Practices

✅ Do’s

// 1. Use standalone components
@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [CommonModule, RouterLink],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `...`
})

// 2. Use signals for reactive state
export class UserCardComponent {
  // Input signals
  user = input.required<User>();
  showActions = input(true);
  
  // Output signals
  edit = output<User>();
  delete = output<string>();
  
  // Computed values
  fullName = computed(() => 
    `${this.user().firstName} ${this.user().lastName}`
  );
  
  // Local state
  isExpanded = signal(false);
}

// 3. Keep components small and focused
// Each component should do ONE thing well

// 4. Use OnPush change detection everywhere
changeDetection: ChangeDetectionStrategy.OnPush

// 5. Prefix selectors consistently
selector: 'app-user-card'  // app- prefix for application
selector: 'lib-button'     // lib- prefix for library

❌ Don’ts

// ❌ Don't use any type
data: any; // Bad
data: User; // Good

// ❌ Don't subscribe in components without cleanup
ngOnInit() {
  this.service.getData().subscribe(data => {}); // Memory leak!
}

// ✅ Do use takeUntilDestroyed
private destroyRef = inject(DestroyRef);

ngOnInit() {
  this.service.getData()
    .pipe(takeUntilDestroyed(this.destroyRef))
    .subscribe(data => {});
}

// ❌ Don't access DOM directly
document.getElementById('myElement'); // Bad

// ✅ Do use template references
@ViewChild('myElement') elementRef: ElementRef;

// ❌ Don't mutate input data
ngOnInit() {
  this.user.name = 'New'; // Bad - mutating input
}

// ✅ Do emit changes through outputs
updateName(name: string) {
  this.userChanged.emit({ ...this.user(), name });
}

Service Best Practices

// ✅ Good service design
@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  private readonly API_URL = '/api/users';
  
  // Use signals for shared state
  private usersState = signal<User[]>([]);
  private loadingState = signal(false);
  
  // Expose as readonly
  readonly users = this.usersState.asReadonly();
  readonly loading = this.loadingState.asReadonly();
  
  // Clear method signatures
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.API_URL);
  }
  
  getUserById(id: string): Observable<User> {
    return this.http.get<User>(`${this.API_URL}/${id}`);
  }
  
  createUser(user: CreateUserDto): Observable<User> {
    return this.http.post<User>(this.API_URL, user);
  }
  
  updateUser(id: string, updates: UpdateUserDto): Observable<User> {
    return this.http.patch<User>(`${this.API_URL}/${id}`, updates);
  }
  
  deleteUser(id: string): Observable<void> {
    return this.http.delete<void>(`${this.API_URL}/${id}`);
  }
}

// ✅ Separate concerns
@Injectable({ providedIn: 'root' })
export class UserStore {
  private userService = inject(UserService);
  
  // State
  private state = signal<UserState>({
    users: [],
    selectedUser: null,
    loading: false,
    error: null
  });
  
  // Selectors
  readonly users = computed(() => this.state().users);
  readonly selectedUser = computed(() => this.state().selectedUser);
  readonly loading = computed(() => this.state().loading);
  
  // Actions
  loadUsers() {
    this.patchState({ loading: true, error: null });
    
    this.userService.getUsers().subscribe({
      next: (users) => this.patchState({ users, loading: false }),
      error: (error) => this.patchState({ error, loading: false })
    });
  }
  
  private patchState(patch: Partial<UserState>) {
    this.state.update(state => ({ ...state, ...patch }));
  }
}

Template Best Practices

<!-- ✅ Use new control flow syntax (Angular 17+) -->
@if (user()) {
  <app-user-card [user]="user()" />
} @else {
  <app-skeleton />
}

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

@switch (status()) {
  @case ('loading') { <app-spinner /> }
  @case ('error') { <app-error /> }
  @default { <app-content /> }
}

<!-- ✅ Use defer for heavy components -->
@defer (on viewport) {
  <app-heavy-chart [data]="chartData()" />
} @placeholder {
  <div class="chart-placeholder"></div>
} @loading (minimum 200ms) {
  <app-spinner />
}

<!-- ✅ Avoid complex logic in templates -->
<!-- ❌ Bad -->
<div *ngIf="items.filter(i => i.active).length > 0">

<!-- ✅ Good - use computed signal -->
<div *ngIf="hasActiveItems()">

<!-- ✅ Use async pipe for observables -->
@if (user$ | async; as user) {
  <app-user-profile [user]="user" />
}

<!-- ✅ Use semantic HTML -->
<article class="card">
  <header>...</header>
  <main>...</main>
  <footer>...</footer>
</article>

<!-- ✅ Add accessibility attributes -->
<button 
  (click)="toggle()"
  [attr.aria-expanded]="isExpanded()"
  aria-controls="panel"
>
  Toggle
</button>

RxJS Best Practices

// ✅ Use proper operators
searchTerm$ = this.searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  filter(term => term.length >= 2),
  switchMap(term => this.search(term)) // Cancel previous requests
);

// ✅ Handle errors properly
this.http.get<Data>(url).pipe(
  catchError(error => {
    this.errorService.handle(error);
    return EMPTY; // or of(fallbackValue)
  })
);

// ✅ Use shareReplay for shared observables
user$ = this.http.get<User>(url).pipe(
  shareReplay({ bufferSize: 1, refCount: true })
);

// ✅ Complete subscriptions
private destroy$ = new Subject<void>();

ngOnInit() {
  this.source$.pipe(
    takeUntil(this.destroy$)
  ).subscribe();
}

ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

// ✅ Or use DestroyRef (recommended)
private destroyRef = inject(DestroyRef);

ngOnInit() {
  this.source$.pipe(
    takeUntilDestroyed(this.destroyRef)
  ).subscribe();
}

// ✅ Prefer higher-order operators over nested subscribes
// ❌ Bad
this.getUser().subscribe(user => {
  this.getPosts(user.id).subscribe(posts => {});
});

// ✅ Good
this.getUser().pipe(
  switchMap(user => this.getPosts(user.id))
).subscribe(posts => {});

Performance Checklist

OnPush Everywhere

changeDetection: ChangeDetectionStrategy.OnPush

Track By for Lists

@for (item of items(); track item.id)

Lazy Load Routes

loadChildren: () => import('./feature')

Defer Heavy Components

@defer (on viewport) { ... }

Signals for State

count = signal(0);
doubled = computed(() => count() * 2);

Virtual Scrolling

<cdk-virtual-scroll-viewport>

Naming Conventions

// Files
user.component.ts
user.service.ts
user.directive.ts
user.pipe.ts
user.guard.ts
user.interceptor.ts
user.model.ts
user.routes.ts

// Classes
export class UserComponent {}
export class UserService {}
export class HighlightDirective {}
export class DateFormatPipe {}
export function authGuard(): CanActivateFn {}
export function loggingInterceptor(): HttpInterceptorFn {}

// Interfaces/Types
export interface User {}
export type UserRole = 'admin' | 'user';
export interface CreateUserDto {}
export interface UpdateUserDto {}

// Constants
export const API_BASE_URL = '/api';
export const DEFAULT_PAGE_SIZE = 20;

// Signals
users = signal<User[]>([]);
isLoading = signal(false);
selectedUserId = signal<string | null>(null);

// Observables (suffix with $)
users$ = this.userService.getUsers();

Testing Guidelines

// ✅ Test component behavior, not implementation
describe('UserCardComponent', () => {
  it('should display user name', () => {
    const fixture = TestBed.createComponent(UserCardComponent);
    fixture.componentRef.setInput('user', mockUser);
    fixture.detectChanges();
    
    expect(fixture.nativeElement.textContent).toContain(mockUser.name);
  });
  
  it('should emit edit event when edit button clicked', () => {
    const fixture = TestBed.createComponent(UserCardComponent);
    fixture.componentRef.setInput('user', mockUser);
    fixture.detectChanges();
    
    const editSpy = jest.spyOn(fixture.componentInstance.edit, 'emit');
    
    const button = fixture.nativeElement.querySelector('[data-testid="edit-btn"]');
    button.click();
    
    expect(editSpy).toHaveBeenCalledWith(mockUser);
  });
});

// ✅ Mock dependencies properly
const userServiceMock = {
  getUsers: jest.fn().mockReturnValue(of(mockUsers))
};

TestBed.configureTestingModule({
  providers: [
    { provide: UserService, useValue: userServiceMock }
  ]
});

// ✅ Use data-testid for test selectors
<button data-testid="submit-btn">Submit</button>

// ✅ Test async code properly
it('should load users on init', fakeAsync(() => {
  fixture.detectChanges();
  tick(500); // Wait for debounce
  
  expect(component.users()).toHaveLength(3);
}));

Security Best Practices

// ✅ Sanitize user input
@Pipe({ name: 'safeHtml', standalone: true })
export class SafeHtmlPipe {
  private sanitizer = inject(DomSanitizer);
  
  transform(html: string): SafeHtml {
    return this.sanitizer.bypassSecurityTrustHtml(html);
  }
}

// ✅ Use HttpOnly cookies for tokens
// Backend sets: Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict

// ✅ Implement CSRF protection
provideHttpClient(
  withXsrfConfiguration({
    cookieName: 'XSRF-TOKEN',
    headerName: 'X-XSRF-TOKEN'
  })
)

// ✅ Validate on both client and server
// ✅ Use Content Security Policy headers
// ✅ Avoid storing sensitive data in localStorage

Code Review Checklist

1

Component Design

  • Single responsibility
  • OnPush change detection
  • Input/Output signals
  • Proper cleanup
2

TypeScript

  • No any types
  • Proper interfaces/types
  • Readonly where appropriate
  • Null checks
3

Performance

  • Lazy loading
  • TrackBy for loops
  • Memoized computations
  • No memory leaks
4

Accessibility

  • Semantic HTML
  • ARIA attributes
  • Keyboard navigation
  • Color contrast
5

Testing

  • Unit tests
  • Integration tests
  • Edge cases covered
  • Mocks appropriate

Summary

Following these best practices ensures:
  • Maintainability: Code is easy to read and modify
  • Performance: Applications run fast and efficiently
  • Scalability: Architecture supports growth
  • Security: Applications are protected against common vulnerabilities
  • Testability: Code is easy to test and verify

Next: Enterprise Capstone Project

Apply everything in a comprehensive enterprise project