AngularJSAngular Architecture

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:

  1. Components — the UI layer
  2. Templates — the view for each component
  3. Services — the business logic / data layer
  4. Dependency Injection (DI) — the system that connects services to components
  5. Modules (NgModules) / Standalone APIs — organize and compile the application
  6. Routing — navigation between views
  7. 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.

Text
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 @Component decorator)
Components

A component is defined by the @Component decorator applied to a TypeScript class:

TS
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

HTML
<!-- 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:

TS
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:

  1. You register a provider (e.g., providedIn: 'root' or in a component's providers array).
  2. Angular's injector creates one instance of the service (or per the scope you defined).
  3. Any component or service that declares the dependency in its constructor receives the same instance.

TS
@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;
    });
  }
}
Note
With Angular's inject() function (Angular 14+), you can also inject outside of constructors — useful in standalone functions and signal-based patterns.
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

TS
// --- 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
  1. The browser loads index.html, which references the compiled main.js bundle.

  2. main.ts calls bootstrapApplication(AppComponent, appConfig) (standalone) or platformBrowserDynamic().bootstrapModule(AppModule) (NgModule).

  3. Angular creates the root injector and registers all providers.

  4. Angular finds the <app-root> element in index.html and renders AppComponent into it.

  5. The component tree is built top-down, triggering lifecycle hooks as components initialize.

  6. Change detection starts running (via Zone.js or manual triggers).

TS
// 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));

TS
// 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)

HTML
<!-- 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:

HTML
<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:

  1. Parent → Child: via @Input() property binding
  2. Child → Parent: via @Output() + EventEmitter
  3. Sibling → Sibling: via a shared Service (or a state management solution like NgRx)
  4. Globally: via Signals, BehaviorSubject in a service, or NgRx store

TS
// 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

Tip
Think of Angular architecture as: Components = what users see, Services = what the app knows, DI = how they connect.