Best Practices Overview
Estimated Time: 2 hours | Difficulty: All Levels | Prerequisites: Complete Angular Course
Project Structure
Copy
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
Copy
// 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
Copy
// ❌ 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
Copy
// ✅ 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
Copy
<!-- ✅ 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
Copy
// ✅ 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
Copy
changeDetection: ChangeDetectionStrategy.OnPush
Track By for Lists
Copy
@for (item of items(); track item.id)
Lazy Load Routes
Copy
loadChildren: () => import('./feature')
Defer Heavy Components
Copy
@defer (on viewport) { ... }
Signals for State
Copy
count = signal(0);
doubled = computed(() => count() * 2);
Virtual Scrolling
Copy
<cdk-virtual-scroll-viewport>
Naming Conventions
Copy
// 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
Copy
// ✅ 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
Copy
// ✅ 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
anytypes - 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