> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 32. Best Practices

> Angular coding standards, architecture patterns, and style guide

<Frame>
  <img src="https://mintcdn.com/devweeekends/AEOaWh79Ur7CdHHv/images/courses/angular-crash-course/angular-architecture.svg?fit=max&auto=format&n=AEOaWh79Ur7CdHHv&q=85&s=d182e6a63e3b3c9b80c20d8b866c7812" alt="Best Practices" width="1000" height="600" data-path="images/courses/angular-crash-course/angular-architecture.svg" />
</Frame>

## Best Practices Overview

<Info>
  **Estimated Time**: 2 hours | **Difficulty**: All Levels | **Prerequisites**: Complete Angular Course
</Info>

This guide consolidates Angular best practices, coding standards, and architectural patterns to help you write maintainable, scalable, and performant applications.

**A note on "best practices"**: These are not religious commandments -- they are battle-tested defaults. Every rule here has exceptions, and a senior engineer's job is to know when to break them and why. The project structure below works for apps with 10-100 features. A tiny 3-page app does not need the `core/shared/features` split. A massive enterprise app with 20 teams might need Nx workspaces instead. Use these patterns as a starting point and adapt to your context.

***

## 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

```typescript theme={null}
// 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

```typescript theme={null}
// ❌ Don't use any type
data: any; // Bad
data: User; // Good

// ❌ Don't subscribe in components without cleanup.
// This subscription lives forever, even after the component is destroyed.
// If the user navigates away and back 10 times, you have 10 zombie subscriptions.
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

```typescript theme={null}
// ✅ 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

```html theme={null}
<!-- ✅ 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

```typescript theme={null}
// ✅ 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 -- nested subscribes create "callback hell" and make
// error handling and unsubscription nearly impossible to get right.
// Each inner subscribe also needs its own cleanup logic.
this.getUser().subscribe(user => {
  this.getPosts(user.id).subscribe(posts => {});
});

// ✅ Good -- the operator chain is flat, error handling propagates
// naturally, and a single takeUntilDestroyed at the end cleans up everything.
this.getUser().pipe(
  switchMap(user => this.getPosts(user.id))
).subscribe(posts => {});
```

***

## Performance Checklist

<CardGroup cols={2}>
  <Card title="OnPush Everywhere" icon="bolt">
    ```typescript theme={null}
    changeDetection: ChangeDetectionStrategy.OnPush
    ```
  </Card>

  <Card title="Track By for Lists" icon="list">
    ```html theme={null}
    @for (item of items(); track item.id)
    ```
  </Card>

  <Card title="Lazy Load Routes" icon="download">
    ```typescript theme={null}
    loadChildren: () => import('./feature')
    ```
  </Card>

  <Card title="Defer Heavy Components" icon="clock">
    ```html theme={null}
    @defer (on viewport) { ... }
    ```
  </Card>

  <Card title="Signals for State" icon="wave-square">
    ```typescript theme={null}
    count = signal(0);
    doubled = computed(() => count() * 2);
    ```
  </Card>

  <Card title="Virtual Scrolling" icon="scroll">
    ```html theme={null}
    <cdk-virtual-scroll-viewport>
    ```
  </Card>
</CardGroup>

***

## Naming Conventions

```typescript theme={null}
// 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

```typescript theme={null}
// ✅ 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

```typescript theme={null}
// ✅ 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

<Steps>
  <Step title="Component Design">
    * [ ] Single responsibility
    * [ ] OnPush change detection
    * [ ] Input/Output signals
    * [ ] Proper cleanup
  </Step>

  <Step title="TypeScript">
    * [ ] No `any` types
    * [ ] Proper interfaces/types
    * [ ] Readonly where appropriate
    * [ ] Null checks
  </Step>

  <Step title="Performance">
    * [ ] Lazy loading
    * [ ] TrackBy for loops
    * [ ] Memoized computations
    * [ ] No memory leaks
  </Step>

  <Step title="Accessibility">
    * [ ] Semantic HTML
    * [ ] ARIA attributes
    * [ ] Keyboard navigation
    * [ ] Color contrast
  </Step>

  <Step title="Testing">
    * [ ] Unit tests
    * [ ] Integration tests
    * [ ] Edge cases covered
    * [ ] Mocks appropriate
  </Step>
</Steps>

***

## 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

***

<Card title="Next: Enterprise Capstone Project" icon="arrow-right" href="/courses/angular-crash-course/33-enterprise-capstone">
  Apply everything in a comprehensive enterprise project
</Card>
