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.
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.
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
// 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.
// 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
// ✅ 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 -- 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 => {});
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
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