Angular Architecture
Understanding Angular's architecture before writing code will save you hours of confusion. Angular is an opinionated framework — it has a specific way of organizing code, and every concept fits into a larger mental model.
The fundamental architectural building blocks of an Angular application are:
- Components — the UI layer
- Templates — the view for each component
- Services — the business logic / data layer
- Dependency Injection (DI) — the system that connects services to components
- Modules (NgModules) / Standalone APIs — organize and compile the application
- Routing — navigation between views
- Directives & Pipes — extend HTML templates
The Component Tree
Every Angular application is a tree of components. At the root sits AppComponent. Every other component is a child, grandchild, or deeper descendant.
AppComponent ├── HeaderComponent │ ├── LogoComponent │ └── NavComponent ├── MainComponent │ ├── SidebarComponent │ └── ContentComponent │ ├── ArticleListComponent │ │ └── ArticleCardComponent (× many) │ └── PaginationComponent └── FooterComponent
Each component owns:
- A TypeScript class (data + logic)
- An HTML template (view)
- An optional CSS stylesheet (scoped styles)
- Angular metadata (via
@Componentdecorator)
Components
A component is defined by the @Component decorator applied to a TypeScript class:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user-card', // <app-user-card> in templates
standalone: true,
template: `
<div class="card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
`,
styles: [`
.card { border: 1px solid #ccc; padding: 1rem; border-radius: 4px; }
`],
})
export class UserCardComponent {
@Input() user!: { name: string; email: string };
}Templates
An Angular template is HTML enhanced with Angular syntax. Angular compiles templates into optimized JavaScript during the build step (Ahead-of-Time compilation).
Template syntax includes:
- Interpolation:
{{ expression }}— output a value as text - Property binding:
[property]="expression"— set a DOM property - Event binding:
(event)="handler()"— respond to DOM events - Two-way binding:
[(ngModel)]="value"— sync model ↔ view - New control flow (Angular 17+):
@if,@for,@switch,@defer
<!-- Angular template syntax examples -->
<h1>{{ title }}</h1> <!-- interpolation -->
<img [src]="imageUrl" [alt]="imageAlt" /> <!-- property binding -->
<button (click)="onClick()">Click</button> <!-- event binding -->
<input [(ngModel)]="searchTerm" /> <!-- two-way binding -->
@if (isLoggedIn) {
<p>Welcome back, {{ username }}!</p>
} @else {
<a href="/login">Sign in</a>
}
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
}Services
A service is a TypeScript class decorated with @Injectable. Services hold business logic, HTTP calls, and shared state — anything that doesn't belong in a component.
Angular uses Dependency Injection to provide services to components. You declare what you need in the constructor, and Angular creates and injects it:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface Product {
id: number;
name: string;
price: number;
}
@Injectable({ providedIn: 'root' }) // singleton, available everywhere
export class ProductService {
private apiUrl = 'https://api.example.com/products';
constructor(private http: HttpClient) {}
getAll(): Observable<Product[]> {
return this.http.get<Product[]>(this.apiUrl);
}
getById(id: number): Observable<Product> {
return this.http.get<Product>(`${this.apiUrl}/${id}`);
}
}Dependency Injection (DI)
Dependency Injection is one of Angular's most powerful features. Instead of creating dependencies inside a class (tight coupling), you declare what you need and Angular provides it (loose coupling).
How it works:
- You register a provider (e.g.,
providedIn: 'root'or in a component'sprovidersarray). - Angular's injector creates one instance of the service (or per the scope you defined).
- Any component or service that declares the dependency in its constructor receives the same instance.
@Component({
selector: 'app-product-list',
standalone: true,
template: `
@for (product of products; track product.id) {
<div>{{ product.name }} — ${{ product.price }}</div>
}
`,
})
export class ProductListComponent implements OnInit {
products: Product[] = [];
// Angular injects ProductService automatically
constructor(private productService: ProductService) {}
ngOnInit() {
this.productService.getAll().subscribe(data => {
this.products = data;
});
}
}NgModules vs Standalone Components
Historically, Angular organized code into NgModules — classes decorated with @NgModule that declare, import, and export components, directives, and pipes.
Since Angular 14, standalone components eliminate the need for NgModules in most cases. A standalone component declares its own imports directly.
Aspect | NgModule approach | Standalone approach |
|---|---|---|
Declaration | Component declared in @NgModule | Component is self-contained |
Imports | Module imports other modules | Component imports what it needs |
Boilerplate | More (extra module file) | Less (everything in @Component) |
Lazy loading | loadChildren: () => import(module) | loadComponent: () => import(component) |
Default since | Angular 2–16 | Angular 17+ |
Recommended | Legacy/large codebases | All new projects |
// --- OLD: NgModule approach ---
@NgModule({
declarations: [UserListComponent, UserCardComponent],
imports: [CommonModule, HttpClientModule],
exports: [UserListComponent],
})
export class UserModule {}
// --- NEW: Standalone approach ---
@Component({
selector: 'app-user-list',
standalone: true,
imports: [UserCardComponent, AsyncPipe], // direct imports
template: `...`,
})
export class UserListComponent {}The Module Bootstrapping Flow
The browser loads index.html, which references the compiled main.js bundle.
main.ts calls bootstrapApplication(AppComponent, appConfig) (standalone) or platformBrowserDynamic().bootstrapModule(AppModule) (NgModule).
Angular creates the root injector and registers all providers.
Angular finds the <app-root> element in index.html and renders AppComponent into it.
The component tree is built top-down, triggering lifecycle hooks as components initialize.
Change detection starts running (via Zone.js or manual triggers).
// main.ts — standalone bootstrap (Angular 17+)
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err));// app.config.ts — application-level providers
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
],
};Directives
Directives extend the behavior of HTML elements. Angular has three kinds:
- Components — directives with a template (the most common type)
- Structural directives — change the DOM structure (
*ngIf,*ngFor, or the new@if,@for) - Attribute directives — change the appearance or behavior of an element (
[ngClass],[ngStyle], custom directives)
<!-- Structural directive (old syntax) -->
<div *ngIf="showPanel">I'm conditionally rendered</div>
<li *ngFor="let item of items">{{ item }}</li>
<!-- Structural directive (new control flow, Angular 17+) -->
@if (showPanel) {
<div>I'm conditionally rendered</div>
}
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
}
<!-- Attribute directive -->
<div [ngClass]="{ active: isActive, disabled: isDisabled }">
Dynamic classes
</div>Pipes
Pipes transform values in templates. They use the | (pipe) symbol:
<p>{{ birthday | date:'longDate' }}</p> <!-- Feb 14, 2024 -->
<p>{{ price | currency:'USD' }}</p> <!-- $19.99 -->
<p>{{ name | uppercase }}</p> <!-- ANGULAR -->
<p>{{ description | slice:0:100 }}</p> <!-- first 100 chars -->
<p>{{ data$ | async }}</p> <!-- unwrap Observable/Promise -->The Data Flow — Unidirectional Architecture
Angular enforces unidirectional data flow:
- Parent → Child: via
@Input()property binding - Child → Parent: via
@Output()+EventEmitter - Sibling → Sibling: via a shared Service (or a state management solution like NgRx)
- Globally: via Signals, BehaviorSubject in a service, or NgRx store
// Parent passes data DOWN via @Input
// Child emits events UP via @Output
@Component({
selector: 'app-counter',
standalone: true,
template: `
<span>Count: {{ count }}</span>
<button (click)="increment()">+</button>
`,
})
export class CounterComponent {
@Input() count = 0;
@Output() countChange = new EventEmitter<number>();
increment() {
this.countChange.emit(this.count + 1);
}
}Architecture Summary
Components are the UI building blocks — they own templates and styles
Services hold business logic and are injected via DI
Standalone components (Angular 17+) replace the need for NgModules in most cases
The Angular Router handles navigation between component views
Data flows down via @Input, events flow up via @Output, shared state lives in services/signals
Pipes transform data in templates; directives extend HTML behavior
Everything compiles Ahead-of-Time for maximum performance