| technology | Angular | ||||||
|---|---|---|---|---|---|---|---|
| domain | frontend | ||||||
| level | Senior/Architect | ||||||
| version | 20+ | ||||||
| tags |
|
||||||
| ai_role | Senior Angular Architecture Expert | ||||||
| last_updated | 2026-03-22 |
- Primary Goal: Provide architectural best practices for Angular including DI usage, modules, routing, and guards.
- Target Tooling: Cursor, Windsurf, Antigravity.
- Tech Stack Version: Angular 20
Note
Context: Tree Shaking
@NgModule({ providers: [MyService] })The service is included in the bundle even if it is not used.
@Injectable({ providedIn: 'root' })Always use providedIn: 'root'. This allows the bundler to remove unused services (Tree Shaking).
Note
Context: Routing Security
@Injectable()
export class AuthGuard implements CanActivate { ... }Class-based guards require more code and injections. They are less flexible for composition.
export const authGuard: CanActivateFn = (route, state) => {
return inject(AuthService).isLoggedIn();
};Use functional Guards (CanActivateFn). They are concise, easy to test, and composable.
Note
Context: HTTP Requests
@Injectable()
export class TokenInterceptor implements HttpInterceptor { ... }Similar to guards: lots of boilerplate, complex registration in the providers array.
export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
return next(req.clone({ setHeaders: { Authorization: token } }));
};Use functional Interceptors (HttpInterceptorFn) with provideHttpClient(withInterceptors([...])).
Note
Context: Data Integrity
updateUser(user: User) {
this.currentUser = user; // Mutable assignment
}Object mutations complicate change tracking and can lead to unpredictable behavior in components using the OnPush strategy.
currentUser = signal<User | null>(null);
updateUser(user: User) {
this.currentUser.set({ ...user }); // Immutable update
}Use Signals for state management. They guarantee reactivity and atomicity of updates.
Note
Context: Rendering Performance
@for (item of items; track getItemId(item))The tracking function is called for each element during every re-render.
@for (item of items; track item.id)Important
Use an object property (ID or a unique key) directly. If a function is needed, it must be as unambiguous and pure as possible.
Note
Context: Component Metadata
@HostListener('click') onClick() { ... }
@HostBinding('class.active') isActive = true;Decorators increase class size and scatter host configuration across the file.
@Component({
host: {
'(click)': 'onClick()',
'[class.active]': 'isActive()'
}
})Use the host property in component metadata. This centralizes all host element settings.
Note
Context: Dynamic Rendering
const factory = this.resolver.resolveComponentFactory(MyComponent);
this.container.createComponent(factory);ComponentFactoryResolver is deprecated. It is an old imperative API.
this.container.createComponent(MyComponent);
// Or strictly in template
<ng-container *ngComponentOutlet="componentSignal()" />Use ViewContainerRef.createComponent directly with the component class or the ngComponentOutlet directive.
Note
Context: Modular Architecture
SharedModule imports and exports all UI components, pipes, and directives.
If a component needs a single button, it is forced to pull the entire SharedModule. This breaks Tree Shaking and increases the initial bundle size.
Import only what is needed directly into the imports of the Standalone component.
Abandon SharedModule in favor of granular imports of Standalone entities.
Note
Context: Architecture
Service A injects Service B, which injects Service A.
Leads to runtime errors ("Cannot instantiate cyclic dependency"). Indicates poor architectural design.
Use forwardRef() as a crutch, but it's better to extract the shared logic into a third Service C.
Refactoring: break services into smaller ones following SRP (Single Responsibility Principle).
Note
Context: Separation of Concerns
A Pipe performs HTTP requests or complex business logic.
Pipes are intended for data transformation in the template. Side effects in pipes violate function purity and kill CD performance.
Important
Pipes MUST be "Pure" (without side effects) and performant (O(1) or O(n)).
Extract logic into services/signals. Leave only formatting to pipes.
Note
Context: TypeScript Safety
getData(): Observable<any> { ... }any disables type checking, nullifying the benefits of TypeScript. Errors only surface at runtime.
getData(): Observable<UserDto> { ... }Use DTO interfaces (generate them from Swagger/OpenAPI) and Zod for API response validation.
Note
Context: RxJS Subscriptions
<div *ngIf="user$ | async as user">{{ (user$ | async).name }}</div>Each async pipe creates a new subscription. This can lead to duplicated HTTP requests.
@if (user$ | async; as user) {
<div>{{ user.name }}</div>
}Use aliases in the template (as varName) or convert the stream to a signal (toSignal).
Note
Context: DI Scopes
@Injectable({ providedIn: 'any' })Creates a new service instance for each lazy-loaded module. This is often unexpected behavior, leading to state desynchronization (different singleton instances).
providedIn: 'root' or providing at the level of a specific component (providers: []).
Avoid any. Explicitly control the scope: either global (root) or local.
Note
Context: Navigation
this.router.navigateByUrl('/users/' + id);Hardcoding route strings makes route refactoring a pain.
this.router.navigate(['users', id]);Use an array of segments. It is safer (automatic encoding of URL parameters) and cleaner.
Note
Context: Change Detection Strategy
Default components (ChangeDetectionStrategy.Default).
Angular checks this component on every app event, even if the component data hasn't changed.
changeDetection: ChangeDetectionStrategy.OnPushAlways set OnPush. With signals, this becomes the de facto standard, as updates occur precisely.
Note
Context: Template Performance
<div>{{ calculateTotal(items) }}</div>The calculateTotal function is called during every Change Detection (CD) cycle, even if items have not changed. This kills UI performance.
total = computed(() => this.calculateTotal(this.items()));<div>{{ total() }}</div>Extract logic into computed() signals or Pure Pipes. They are only executed when input data changes.
Note
Context: RxJS Memory Leaks
destroy$ = new Subject<void>();
ngOnDestroy() { this.destroy$.next(); }
stream$.pipe(takeUntil(this.destroy$)).subscribe();It's easy to forget takeUntil or unsubscribe. Requires a lot of boilerplate code in every component.
stream$.pipe(takeUntilDestroyed()).subscribe();Use the takeUntilDestroyed() operator. It automatically unsubscribes upon context destruction (component, directive, service).
Note
Context: Prop Drilling
<!-- Parent -->
<app-child [theme]="theme"></app-child>
<!-- Child -->
<app-grandchild [theme]="theme"></app-grandchild>"Prop drilling" heavily couples intermediate components to data they don't need, just for the sake of passing it deeper.
// Service
theme = signal('dark');
// Grandchild
theme = inject(ThemeService).theme;Use Signal Stores or services for state sharing, or the new input() API with context inheritance (in the future).
Note
Context: Security & Abstraction
el.nativeElement.style.backgroundColor = 'red';Direct DOM access breaks abstraction (doesn't work in SSR/Web Workers) and opens up XSS vulnerabilities. It bypasses Angular Sanitization mechanisms.
// Use Renderer2 or bindings
<div [style.background-color]="color()"></div>Note
Context: Change Detection
The application relies on Zone.js for any asynchronous event (setTimeout, Promise, XHR).
Zone.js patches all browser APIs, adding overhead and increasing bundle size. CD triggers more often than necessary.
bootstrapApplication(App, {
providers: [provideExperimentalZonelessChangeDetection()]
});