AngularAtlasAusg. 22.2

Kap. 13Architektur im großen Maßstab60 / 75

Hexagonale Architektur (Ports & Adapters)

Eine Domäne ohne Angular, Ports als abstrakte Klassen und austauschbare Adapter: HTTP, localStorage oder In-Memory, ohne die UI anzufassen.

Architektur ~0 Min. Lesezeit

Die hexagonale Architektur (oder Ports & Adapters) geht von einer einfachen Idee aus: Die Regeln der Anwendung sollen nicht wissen müssen, woher die Daten kommen oder wie sie angezeigt werden. Die Domäne sitzt in der Mitte, geschrieben in reinem TypeScript. Um sie herum liegen die Use Cases, die mit der Außenwelt nur über Ports sprechen, also über Verträge. Am Rand sitzen die Adapter: HTTP, localStorage, Angular-Komponenten. Du kannst sie austauschen, ohne den Kern anzufassen.

In Angular passt die Idee sehr gut zu DI: Eine abstrakte Klasse ist zugleich der Typ des Ports und das Injection-Token. { provide: TaskRepository, useClass: HttpTaskRepository } ist die ganze „Verdrahtung“, die du brauchst.

Warum das wichtig ist
Ändert sich das Backend (eine andere API, GraphQL, Offline-first), passt du nur einen einzigen Adapter an. Wenn du die Regeln testest, startest du kein HttpClient. Und die Geschäftslogik verteilt sich nicht mehr über die Komponenten, wo sie schwer zu finden und schwer wiederzuverwenden ist.

Die Schichten und die Dependency Rule

Es gibt vier Schichten, und Imports zeigen nur nach innen. Die Domäne importiert nichts von außerhalb. Die Application-Schicht kennt die Domäne und die Ports, aber nicht die Adapter. Adapter implementieren die Ports. Die einzige Stelle, die einen Port an einen konkreten Adapter bindet, ist die Composition Root: app.config.ts oder die providers einer Route.

                 ┌───────────────────────── außen: Adapter ────────────────────────────┐
                 │  presentation (driving)                 infrastructure (driven)     │
  Nutzer ──────▶ │  TasksPage, TaskItem                    HttpTaskRepository, TaskDto │ ──▶ server
                 │        │   ┌──────────── application ─────────────┐   ▲             │
                 │        └──▶│ TaskBoard / AddTask ──▶ TaskRepository│───┘ implementiert│
                 │            │        │      ┌──── domain ────┐     │                  │
                 │            │        └─────▶│ Task, createTask│    │                  │
                 │            │               └─────────────────┘     │                  │
                 │            └───────────────────────────────────────┘                  │
                 └──────────────────────────────────────────────────────────────────────┘
  Regel: Import-Pfeile zeigen NUR nach innen. Die Domäne importiert nichts von außen.
  app.config.ts (Composition Root) ist die einzige Stelle, die TaskRepository an einen Adapter bindet.
SchichtEnthältDarf importieren
domainEntitäten, Regeln, Domänenfehlernichts (nur TypeScript)
applicationPorts (abstrakte Klassen), Use Cases, Zustand für die UIdomain, @angular/core (DI, Signals)
infrastructureAdapter: HTTP, Storage, DTOs, Mappingapplication (die Ports), domain, HttpClient
presentationSmart- und Dumb-Komponentenapplication, domain (Typen) — niemals infrastructure
LiveDependency Rule: Ist dieser Import erlaubt?

1. Wähle die Datei, die importiert.

presentation · Driving-Adapter
infrastructure · Driven-Adapter
application · Ports + Use Cases
domain · reines TypeScript
framework & browser
composition root
Beispiele:
Wähle, woher der Import kommt und was er importiert. Das Urteil zeigt dir, ob die Regel eingehalten wird und was du stattdessen tun solltest.

Domain: reines TypeScript

Die Domäne enthält die Entitäten und die Regeln: „Ein Titel hat mindestens 3 Zeichen“, „zwei offene Aufgaben dürfen nicht denselben Titel haben“. Sie importiert weder Angular noch RxJS noch Browser-APIs. Sogar die Uhr und die ID-Erzeugung kommen als Parameter herein, damit die Regeln deterministisch bleiben. Der Hauptgewinn: Die Domäne testest du mit einfachen Funktionen, ohne TestBed und ohne Mocks.

// tasks/domain/task.ts — reines TypeScript: kein Import aus Angular, RxJS oder dem Browser
export type TaskId = string & { readonly __brand: 'TaskId' };

export interface Task {
  readonly id: TaskId;
  readonly title: string;
  readonly done: boolean;
  readonly createdAt: Date;
}

export class DomainError extends Error {
  override readonly name = 'DomainError';
}

export function normalizeTitle(raw: string): string {
  return raw.trim().replace(/\s+/g, ' ');
}

// `now` und `id` kommen als Parameter herein: Die Domäne liest weder die Uhr noch erzeugt sie selbst IDs
export function createTask(raw: string, existing: readonly Task[], now: Date, id: TaskId): Task {
  const title = normalizeTitle(raw);
  if (title.length < 3) throw new DomainError('Der Titel braucht mindestens 3 Zeichen.');
  if (existing.some((t) => !t.done && t.title.toLowerCase() === title.toLowerCase())) {
    throw new DomainError('Es gibt bereits eine offene Aufgabe mit diesem Titel.');
  }
  return { id, title, done: false, createdAt: now };
}

export function toggleTask(task: Task): Task {
  return { ...task, done: !task.done };
}

Ports: abstrakte Klassen als Tokens

Ein Port beschreibt, was die Anwendung braucht („auflisten, speichern, löschen“), nicht wie. Du schreibst ihn als abstrakte Klasse: Sie existiert zur Laufzeit, also funktioniert inject(TaskRepository) direkt. Ein Interface würde beim Kompilieren verschwinden, und du bräuchtest ein separates InjectionToken (siehe InjectionToken). Fehler gehören zum Vertrag: Adapter übersetzen ihre technischen Fehler (Timeout, 503) in RepositoryUnavailableError, damit die UI nicht von HttpErrorResponse abhängt.

// tasks/application/task-repository.ts — der PORT
import { Task, TaskId } from '../domain/task';

// Abstrakte Klasse, kein Interface: Sie existiert zur Laufzeit und ist daher direkt ein DI-Token.
// Sie hat kein @Injectable und kennt keine Implementierung.
export abstract class TaskRepository {
  abstract list(): Promise<readonly Task[]>;
  abstract save(task: Task): Promise<void>; // upsert
  abstract remove(id: TaskId): Promise<void>;
}

// Teil des Vertrags: Adapter übersetzen ihre technischen Fehler hierin.
export class RepositoryUnavailableError extends Error {
  override readonly name = 'RepositoryUnavailableError';
}

Use Cases

Ein Use Case orchestriert: Er liest über einen Port, wendet die Domänenregeln an und speichert über einen Port. In Angular ist er ein gewöhnlicher Service, der den Port injiziert, nicht den Adapter. Für die UI ist ein „Board“ praktisch, das den Zustand in Signals hält (tasks, error, open). Komponenten lesen es nur, genau wie beim State Management.

// tasks/application/add-task.ts — ein Use Case = eine Klasse mit einer einzigen öffentlichen Methode
import { Injectable, inject } from '@angular/core';
import { Task, TaskId, createTask } from '../domain/task';
import { TaskRepository } from './task-repository';

@Injectable({ providedIn: 'root' })
export class AddTask {
  private readonly repo = inject(TaskRepository); // der Port — er weiß nicht, welcher Adapter antwortet

  async execute(title: string): Promise<Task> {
    const existing = await this.repo.list();
    const task = createTask(title, existing, new Date(), crypto.randomUUID() as TaskId); // die Regel: in der Domäne
    await this.repo.save(task); // I/O: über den Port
    return task;
  }
}

Adapter

Ein Adapter extends TaskRepository und erledigt die gesamte I/O: HTTP, localStorage, Speicher. Auch das Mapping von DTO auf Domäne liegt hier (is_done: 1 wird zu done: true, der ISO-String wird zu einem Date; siehe Interfaces & Modelle). Das Format des Servers kommt nicht über den Adapter hinaus.

// tasks/infrastructure/http-task-repository.ts — der HTTP-ADAPTER
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable, firstValueFrom } from 'rxjs';
import { Task, TaskId } from '../domain/task';
import { RepositoryUnavailableError, TaskRepository } from '../application/task-repository';
import { TaskDto, taskFromDto, taskToDto } from './task-dto';

@Injectable()
export class HttpTaskRepository extends TaskRepository {
  private readonly http = inject(HttpClient);
  private readonly url = '/api/tasks';

  async list(): Promise<readonly Task[]> {
    const dtos = await this.call(this.http.get<TaskDto[]>(this.url));
    return dtos.map(taskFromDto); // DTO → Domäne, hier und nirgends sonst
  }

  async save(task: Task): Promise<void> {
    await this.call(this.http.put<TaskDto>(`${this.url}/${task.id}`, taskToDto(task)));
  }

  async remove(id: TaskId): Promise<void> {
    await this.call(this.http.delete<void>(`${this.url}/${id}`));
  }

  // HttpErrorResponse verlässt den Adapter nicht: Der Kern erhält den Fehler des Ports
  private async call<T>(request: Observable<T>): Promise<T> {
    try {
      return await firstValueFrom(request);
    } catch (e) {
      if (e instanceof HttpErrorResponse) throw new RepositoryUnavailableError(`HTTP ${e.status}`);
      throw e;
    }
  }
}
LiveDieselbe UI, drei Adapter

{ provide: TaskRepository, useClass: InMemoryTaskRepository } — Eine Map im Speicher — die Daten verschwinden, sobald der Injector zerstört wird.

Identische UI: TasksPanel → TaskBoard → TaskRepository
  • Keine Aufgaben.
0 offen von 0 fertig
Wer geantwortet hat
  • 11:30.3DI TaskRepository → InMemoryTaskRepository
  • 11:30.4InMemoryTaskRepository list() → 0 (Map)
Wechsle den Adapter und füge Aufgaben hinzu. Die Komponente und die Use Cases sind identisch; das Log zeigt, welcher Adapter jeden Aufruf beantwortet hat. Bei HTTP kannst du den Server offline schalten.

Composition Root: wo du den Adapter wählst

Die Bindung von Port an Adapter sitzt an einer einzigen Stelle. Global in app.config.ts. Lokal in den providers einer Route, wo der Adapter so lange lebt wie die Route (siehe Providers & Injector-Hierarchie). Damit die Details des Features privat bleiben, stellt es eine Funktion provideTasks() bereit. Die Composition Root wählt nur „http“ oder „local“.

// app.config.ts — COMPOSITION ROOT: die einzige Datei, die weiß, welchen Adapter wir nutzen
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
    { provide: TaskRepository, useClass: HttpTaskRepository },
  ],
};

Testing: hier zeigt sich der Gewinn

Du hast drei Testebenen, jede davon günstig:

  • die Domäne, mit reinen Funktionen;
  • die Use Cases, mit dem In-Memory-Adapter anstelle von HTTP;
  • die Adapter, mit einem Contract Test: dieselbe Suite, ausgeführt auf jeder Implementierung des Ports.

Der Contract Test fängt genau die Fehler ab, die sonst durchrutschen würden: einen Adapter, der DTOs statt Entitäten zurückgibt, oder einen, der dupliziert, statt zu aktualisieren.

// Use-Case-Test: der In-Memory-Adapter statt HTTP
import { TestBed } from '@angular/core/testing';

describe('AddTask', () => {
  let addTask: AddTask;
  let repo: InMemoryTaskRepository;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [{ provide: TaskRepository, useClass: InMemoryTaskRepository }],
    });
    addTask = TestBed.inject(AddTask);
    repo = TestBed.inject(TaskRepository) as InMemoryTaskRepository;
  });

  it('speichert über den Port', async () => {
    await addTask.execute('Tests schreiben');
    expect((await repo.list()).map((t) => t.title)).toEqual(['Tests schreiben']);
  });

  it('speichert nichts, wenn eine Domänenregel verletzt wird', async () => {
    await expect(addTask.execute('ab')).rejects.toThrow(DomainError);
    expect(await repo.list()).toEqual([]);
  });
});
LiveDer Contract Test, ausgeführt auf jedem Adapter
6 Tests × 4 Adapter
Test (für alle gleich) In-MemorylocalStorageHTTPHTTP ohne Mapping
ein neues Repository ist leer····
save() und danach list() liefert die Aufgabe····
save() mit derselben id aktualisiert, dupliziert nicht····
remove() löscht nur die angeforderte Aufgabe····
done ist ein Boolean, createdAt ein Date····
liefert Domänenentitäten, keine DTOs····
Führe die Suite aus: Dieselben Tests bestehen bei In-Memory, localStorage und HTTP, scheitern aber beim Adapter „HTTP ohne Mapping“, der DTOs in die Domäne durchlässt.

Wie du die Regel im Team durchsetzt

Eine Architekturregel, die nur in deinem Kopf existiert, überlebt die erste Deadline nicht. Packe sie ins Linting. Mit dem no-restricted-imports von ESLint, aufgeteilt nach Schichten, scheitert jeder falsche Import in der CI. In einem Nx-Monorepo erreichst du dasselbe mit Tags und @nx/enforce-module-boundaries (siehe Monorepo, Libraries & Grenzen und Barrels, Aliase & Grenzen).

// Die Dependency Rule, bei jedem Commit geprüft (ESLint no-restricted-imports mit Regex)
export default [
  {
    files: ['src/app/features/*/domain/**/*.ts'],
    rules: {
      'no-restricted-imports': ['error', { patterns: [
        { regex: '^(@angular/|rxjs)', message: 'Die Domäne ist reines TypeScript: kein Angular, kein RxJS.' },
        { regex: '/(application|infrastructure|ui)/', message: 'Die Domäne hängt nicht von den äußeren Schichten ab.' },
      ] }],
    },
  },
  {
    files: ['src/app/features/*/application/**/*.ts'],
    rules: {
      'no-restricted-imports': ['error', { patterns: [
        { regex: '/(infrastructure|ui)/', message: 'Application hängt von Ports ab, nicht von Adaptern oder der UI.' },
        { regex: '^@angular/common/http', message: 'HTTP nur in infrastructure.' },
      ] }],
    },
  },
  {
    files: ['src/app/features/*/ui/**/*.ts'],
    rules: {
      'no-restricted-imports': ['error', { patterns: [
        { regex: '/infrastructure/', message: 'Die UI injiziert den Port oder den Use Case, nicht den Adapter.' },
      ] }],
    },
  },
];
// In einem Nx-Monorepo: domain/application/infrastructure als separate Libraries, mit Tags
// und @nx/enforce-module-boundaries (siehe die Seite Monorepo).

Wann ja, wann nein

Ports & Adapter lohnen sich
  • Echte Geschäftsregeln (Preise, Validierungen, Abläufe), die getestet und wiederverwendet werden müssen.
  • Ein instabiles oder sich änderndes Backend: eine neue API, eine Migration von REST auf GraphQL, ein Offline-Modus.
  • Dieselbe Logik über mehrere Datenquellen (Server, lokaler Cache, ein Mock für Demos).
  • Große, langlebige Anwendungen, in denen mehrere Teams am selben Feature arbeiten.
Das ist Overkill — halte es einfach
  • CRUD-Screens, die nur anzeigen, was der Server liefert. Ein Service mit httpResource reicht.
  • Prototypen und kleine Apps: Vier Schichten für zwei Formulare bremsen das Team aus.
  • Wenn der „Port“ nur eine einzige Implementierung hat und du ihn nicht separat testest. Die Abstraktion bringt dann nichts.
  • Reine UI-Logik (Hover, Aufklappen): Sie bleibt in der Komponente.

Eine pragmatische Variante funktioniert in den meisten Projekten: eine reine Domäne für die Regeln, die zählen, einen Port nur dort, wo du mehrere Implementierungen hast oder haben wirst, und den Rest in der üblichen data-access- / ui-Struktur (siehe Ordnerstruktur).

Häufige Stolperfallen

Der Port, der den Adapter importiert
@Injectable({ providedIn: 'root', useClass: HttpTaskRepository }) auf der abstrakten Klasse wirkt bequem, aber der Port hängt dann von der Infrastruktur ab, und HttpClient landet im Bundle, auch dort, wo du ihn nicht nutzt. Die Wahl des Adapters gehört in die Composition Root.
// ✗ „Standardimplementierung“ am Port
@Injectable({ providedIn: 'root', useClass: HttpTaskRepository }) // der Port importiert den Adapter!
export abstract class TaskRepository { /* ... */ }
// Application → infrastructure: Die Dependency Rule ist verletzt, und HttpClient
// landet im Bundle, selbst für die App, die nur localStorage nutzt.

// ✓ Der Port bleibt leer; die Wahl liegt in der Composition Root
export abstract class TaskRepository { /* ... */ }
providers: [{ provide: TaskRepository, useClass: HttpTaskRepository }]
  • DTOs, die in die Domäne durchsickern. Wenn der Typ des Servers bis in die Komponenten gelangt, breitet sich jede API-Änderung überall aus. Das Mapping gehört in den Adapter.
  • Komponenten, die den konkreten Adapter injizieren (inject(HttpTaskRepository)). Dann hast du gar keinen Port. Injiziere den Port oder den Use Case.
  • Eine „anämische“ Domäne. Entitäten ohne Regeln, mit der gesamten Logik in Services: Du hast die Schichten, aber nicht den Nutzen.
  • Technische Fehler, die bis zur UI durchdringen. Eine HttpErrorResponse in einer Komponente bedeutet, dass die UI weiß, dass es HTTP ist. Übersetze die Fehler im Adapter.
  • Eine Uhr und IDs, die innerhalb der Domäne erzeugt werden (new Date(), crypto.randomUUID()). Die Regeln werden schwer testbar. Reiche sie als Parameter herein.

Spickzettel

SCHICHTEN (Imports zeigen nur nach innen)
  domain          Task, createTask()         reines TypeScript — kein Angular, RxJS, Browser
  application     TaskRepository (Port)      abstrakte Klasse = Typ + DI-Token
                  AddTask / TaskBoard        inject(TaskRepository), Signals für die UI
  infrastructure  HttpTaskRepository         extends TaskRepository, DTO ↔ Domäne, Fehler übersetzt
  presentation    TasksPage, TaskItem        injiziert application, niemals infrastructure
  composition     app.config / route.providers  { provide: TaskRepository, useClass: Http… }

REZEPTE
  Adapter tauschen         ein anderes useClass in der Composition Root (oder provideTasks('local'))
  Use-Case-Test            providers: [{ provide: TaskRepository, useClass: InMemoryTaskRepository }]
  Adapter-Test             gemeinsamer Contract + HttpTestingController für das HTTP-Mapping
  DTO                      nur in infrastructure; der Port spricht in Entitäten
  Fehler                   der Adapter wirft RepositoryUnavailableError, nicht HttpErrorResponse

WANN NICHT
  CRUD ohne Regeln, kleine App, Prototyp → httpResource in der Komponente + Mapping in parse

Kap. 13 · 0 von 4 abgeschlossen

Lektion abgeschlossen?

Markiere sie, dann erscheint sie auf deiner Karte: im Inhaltsverzeichnis und auf der Startseite.

Wird automatisch markiert, sobald du die ganze Lektion gelesen hast.

  1. Micro Frontends
  2. Web Components mit Angular Elements
  3. Monorepo, Bibliotheken & Grenzen
  4. Hexagonale Architektur (Ports & Adapters)
Deine ganze Karte ansehen

Alle 75 Seiten · tippen zum Filtern

  • 01Was ist neu in Angular 21 & 22Alle wichtigen Änderungen in v21 und v22 in Kürze, mit Link zur Seite, die sie jeweils erklärt.Start
  • 01Deine erste App, Schritt für SchrittEine Leseliste, von einer leeren Komponente bis zur fertigen App — Signals, @for, Inputs und Outputs, ein Service und ein Formular, Schritt für Schritt.Start
  • 01Schnelle Entscheidungen„Wohin damit?“, „Signal oder Observable?“, „Shared oder Feature?“ — kurze Antworten auf Alltagsfragen.Start
  • 01Angular-Interviewfragen60 echte Interviewfragen für Junior, Mid und Senior, mit der passenden Antwort, der Nachfrage und den Warnsignalen. Dazu ein Probeinterview.Start
  • 02Standalone & BootstrapWie eine App ohne NgModule startet: bootstrapApplication, app.config.ts, imports in der Komponente.Moderne Grundlagen
  • 02Data Binding & EventsInterpolation, Property- und Attribut-Bindings, Events mit $event und Tastenfiltern und wie [( )] aufgebaut ist — was in das DOM geht und was zurückkommt.Moderne Grundlagen
  • 02Control Flow: @if, @for, @switchDie Block-Syntax, die *ngIf/*ngFor/ngSwitch ersetzt, mit track, @empty und Exhaustive Checks.Moderne Grundlagen
  • 02@let und die neuen Template-AusdrückeLokale Variablen mit @let, Arrow Functions, Spread, Regex, instanceof, Kommentare in Tags.Moderne Grundlagen
  • 02@defer — Lazy Loading in TemplatesSchwere Komponenten nur laden, wenn sie gebraucht werden: Trigger, Prefetch, Placeholder, Loading, Error.Moderne Grundlagen
  • 02Class- & Style-BindingsWarum [class.x] und [style.x] ngClass und ngStyle abgelöst haben (mit automatischer Migration ab v21).Moderne Grundlagen
  • 02Komponenten-Styles & Kapselung:host, die drei Kapselungsmodi, Theming mit CSS-Variablen und warum ::ng-deep der letzte Ausweg ist.Moderne Grundlagen
  • 02Pipes: wann und wiePure vs. Impure Pipes, eigene Pipes und wie du zwischen Pipe, computed() und Methode wählst.Moderne Grundlagen
  • 03signal() und computed()Die reaktiven Grundbausteine: beschreibbarer Zustand, abgeleiteter Zustand, Gleichheit, untracked und Regeln zur Immutability.Signals & Reaktivität
  • 03effect() — wann ja, wann neinMit Signals synchronisierte Seiteneffekte: localStorage, Logging, imperative APIs. Und ihre Stolperfallen.Signals & Reaktivität
  • 03linkedSignal() — editierbarer abgeleiteter ZustandEin Signal, das sich zurücksetzt, wenn sich seine Quelle ändert, das du aber auch manuell setzen kannst.Signals & Reaktivität
  • 03resource, rxResource, httpResourceAsynchrone Daten als Signals: status, value, error, reload, automatischer Abbruch. Stabil seit v22.Signals & Reaktivität
  • 03debounced() — verzögerte SignalsDie „Debounce“-Variante eines Signals, ohne RxJS. Perfekt für die Suche.Signals & Reaktivität
  • 03Listen: suchen, filtern, sortieren, blätternEine Kette von computed-Signals, die nur neu berechnet, was sich geändert hat — mit den Filtern in der URL, damit ein Link dieselbe Ansicht öffnet.Signals & Reaktivität
  • 03Observables von null anWas ein Observable ist, subscribe und unsubscribe, die Operatoren, die du jede Woche brauchst, cold vs. hot — und wann ein Signal reicht.Signals & Reaktivität
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — und wann RxJS weiterhin die richtige Wahl bleibt.Signals & Reaktivität
  • 04input() und output()Inputs als Signals (required, transform, alias) und typisierte Outputs. Daten nach unten, Events nach oben.Komponenten: Kommunikation
  • 04model() — Two-Way-BindingBanana-in-a-Box [( )] mit einer einzigen API. Wann du es nutzt und wann input + output besser ist.Komponenten: Kommunikation
  • 04viewChild & contentChildQueries als Signals: viewChild, viewChildren, contentChild, contentChildren, die Option read.Komponenten: Kommunikation
  • 04Lifecycle & DestroyRefWelche Hooks in der Signals-Ära noch zählen, afterNextRender, DestroyRef und typisiertes ngOnChanges.Komponenten: Kommunikation
  • 04Content Projectionng-content mit select, mehrere Slots, Standardinhalt und die Falle der bedingten Projektion.Komponenten: Kommunikation
  • 04ng-template & ngTemplateOutletTemplates als Werte: Render Props in Angular, typisierter Kontext und Anpassen von Komponenten.Komponenten: Kommunikation
  • 05Smart vs. DumbContainer vs. Presentational: wer den Zustand hält, wer nur anzeigt und wie du die beiden sauber trennst.Komponenten-Design
  • 05Generische Komponenten <T>Eine korrekt typisierte DataTable<T> oder Select<T>, mit Templates für Zellen.Komponenten-Design
  • 05Dynamische KomponentenNgComponentOutlet, createComponent mit inputBinding/outputBinding und ein Dashboard, das aus einer Konfiguration gebaut wird.Komponenten-Design
  • 05Direktiven & Host-KompositionAttribut-Direktiven, die host: {}-Metadaten und die Directive Composition API (hostDirectives).Komponenten-Design
  • 05Eigene Formular-ControlsFormValueControl (Signal Forms) vs. ControlValueAccessor: eine Sternebewertung, in beiden Varianten.Komponenten-Design
  • 05Change DetectionOnPush ist ab v22 der Standard, zone.js entfällt ab v21. Was das Rendering auslöst und wie du dir nicht die Finger verbrennst.Komponenten-Design
  • 06inject() & @Service()Der neue @Service()-Decorator ab v22, inject() vs. Konstruktor und der Injection-Kontext.Dependency Injection
  • 06Provider & die Injector-HierarchieRoot vs. Route vs. Komponente, useClass/useValue/useFactory/useExisting und die Modifier optional/self/skipSelf.Dependency Injection
  • 06InjectionToken & provideX()Typisierte Konfiguration, provideFeature()-Funktionen, makeEnvironmentProviders und Initializer.Dependency Injection
  • 06injectAsync() — Lazy ServicesLade einen schweren Service erst, wenn du ihn brauchst, mit Prefetch im Leerlauf.Dependency Injection
  • 07Ordnerstrukturcore / shared / features, was in welchen Ordner gehört und wie ein Feature von innen aussieht.Architektur der Anwendung
  • 07Shared vs. FeatureWann du eine Komponente nach Shared verschiebst und (vor allem) wann NICHT. Eine konkrete Checkliste.Architektur der Anwendung
  • 07Interfaces & ModelleInline-Interface oder eigene Datei? DTO vs. Domänenmodell, type vs. interface, wohin damit.Architektur der Anwendung
  • 07State Management mit SignalsEinfache Signal-Stores, Fassaden, lokaler vs. globaler Zustand und wann du NgRx brauchst.Architektur der Anwendung
  • 07Styleguide & NamensgebungDie aktuellen offiziellen Konventionen: Dateinamen ohne Suffixe, protected, readonly, Reihenfolge der Member.Architektur der Anwendung
  • 07Barrels, Aliase & Grenzenindex.ts, Pfad-Aliase, die öffentliche API eines Features und Abhängigkeitsregeln, die sich automatisch prüfen lassen.Architektur der Anwendung
  • 07Fehler: @boundary & ErrorHandlerDer neue @boundary-Block ab v22.2, der globale ErrorHandler, HTTP-Fehler und Fehler aus resource.Architektur der Anwendung
  • 08Modernes Routing & Lazy LoadingloadComponent, Child Routes, Parameter als Inputs und Parameter-Vererbung (neuer Standard in v22).Routing
  • 08Guards & ResolvercanActivate, canMatch, canDeactivate und resolve als Funktionen, mit Redirects über UrlTree / RedirectCommand.Routing
  • 09Signal Forms — die GrundlagenFormulare auf Basis von Signals: form(), [formField], schemabasierte Validierung. Stabil seit v22.Formulare
  • 09Signal Forms — fortgeschrittenEigene und feldübergreifende Validierung, async/HTTP mit Debounce, dynamische Listen, disabled/hidden, wiederverwendbare Schemas.Formulare
  • 09Typisierte Reactive FormsTypisierte FormGroup, nonNullable, FormArray — und wie sie neben Signal Forms bestehen.Formulare
  • 10HttpClient, Interceptors & die API-SchichtHttpClient ohne Setup (v21), Fetch als Standard (v22), funktionale Interceptors und eine API-Schicht mit DTO-Mapping.HTTP & Daten
  • 11Performance — eine ChecklisteWas wirklich zählt: OnPush + Signals, track, @defer, Lazy Routes, Bilder, Bundles und Hydration.Qualität & Performance
  • 11Testen mit VitestVitest ist ab v21 der Standard-Runner. TestBed, Komponenten mit Signals, Services, HTTP und Harnesses.Qualität & Performance
  • 11DevTools: ein Blick in deine AppChrome DevTools und Angular DevTools Schritt für Schritt: was jedes Panel zeigt, mit Übungen, die auf dieser Website funktionieren.Qualität & Performance
  • 11Angular AriaHeadless, barrierefreie Komponenten, die du nach Belieben stylst: Tabs, Accordion, Menü, Listbox...Qualität & Performance
  • 12SSR, Prerendering & Render ModesWann auf dem Server gerendert wird, wann beim Build und wann nur im Browser: RenderMode pro Route, app.routes.server.ts und die echten Kosten.SSR & Hydration
  • 12Hydration: Event Replay & inkrementellWie der Browser das Server-HTML übernimmt, ohne es neu zu rendern: vollständige Hydration, Event Replay, @defer (hydrate on …) und Mismatch-Fehler.SSR & Hydration
  • 12Servertauglicher Codewindow gibt es auf dem Server nicht: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, der HTTP-Cache und PendingTasks.SSR & Hydration
  • 13Micro FrontendsWann es sich lohnt, eine App in mehrere Deployments aufzuteilen und wann es nur Komplexität ist: Native Federation, Module Federation, iframes, Web Components.Architektur im großen Maßstab
  • 13Web Components mit Angular ElementscreateCustomElement: eine Angular-Komponente, die überall als HTML-Tag nutzbar ist, plus Kommunikation zwischen Apps über Events und BroadcastChannel.Architektur im großen Maßstab
  • 13Monorepo, Bibliotheken & GrenzenEin Workspace mit mehreren Apps und Bibliotheken: ng generate library, Nx, Secondary Entry Points, Publishing und Abhängigkeitsregeln.Architektur im großen Maßstab
  • 13Hexagonale Architektur (Ports & Adapters)Eine Domäne ohne Angular, Ports als abstrakte Klassen und austauschbare Adapter: HTTP, localStorage oder In-Memory, ohne die UI anzufassen.Architektur im großen Maßstab
  • 14Signals unter der HaubeDer reaktive Graph: Producer und Consumer, Push-Pull, warum computed lazy und glitch-frei ist, das Diamond-Problem und wann Effects laufen.Fortgeschrittene Reaktivität
  • 14Fortgeschrittenes RxJS: Operatoren höherer OrdnungswitchMap, mergeMap, concatMap und exhaustMap an einem Live-Diagramm, dazu Retry mit Backoff, shareReplay und wie du Leaks vermeidest.Fortgeschrittene Reaktivität
  • 14NgRx SignalStoreState Management im großen Stil auf Signals: withState, withComputed, withMethods, rxMethod, Entities und Hooks — und wann ein einfacher Store reicht.Fortgeschrittene Reaktivität
  • 14Zustandsautomaten mit SignalsKeine unmöglichen Zustände mehr: ein typisierter Automat auf Signals, mit Übergängen, Guards und einem Live-Zustandsdiagramm.Fortgeschrittene Reaktivität
  • 15WebSockets: Echtzeit, die das Netz überstehtEin typisierter Socket als Signals: Reconnect mit Backoff und Jitter, Heartbeat, ein Postausgang im Offline-Modus und SSR-Sicherheit.Echtzeit & offline
  • 15Optimistische Updates & RollbackEine sofortige UI, die ehrlich bleibt: Snapshot und Rollback, Antworten in falscher Reihenfolge, Idempotenzschlüssel.Echtzeit & offline
  • 15Offline-first: Service Worker & CachingDer Angular Service Worker: Caching-Strategien, Update-Hinweise und ein Postausgang für Änderungen, die offline entstehen.Echtzeit & offline
  • 16Web WorkersSchwere Berechnungen, ohne die UI zu blockieren: ein echter Worker in der Seite, Message Passing und wann es sich nicht lohnt.Fortgeschrittene UI & Performance
  • 16Riesige Listen: Virtual Scroll100.000 Zeilen ohne Ruckeln: CDK Virtual Scroll, itemSize, trackBy und die Alternativen (Paginierung, Infinite Scroll).Fortgeschrittene UI & Performance
  • 16Moderne Animationen & View Transitionsanimate.enter und animate.leave statt des veralteten @angular/animations, CSS-Transitions und View Transitions bei der Navigation.Fortgeschrittene UI & Performance
  • 16Memory Leaks finden und behebenSubscriptions, Timer und Listener, die ihre Komponente überleben – wie du sie in den DevTools findest und schließt.Fortgeschrittene UI & Performance
  • 17Sicherheit: XSS, Sanitizing & CSPWas Angular automatisch gegen XSS tut, wann bypassSecurityTrust gefährlich wird, dazu CSP mit Nonce, Trusted Types und XSRF.Sicherheit & Globalisierung
  • 17Authentifizierung: Tokens & RefreshEin Interceptor, der das Token anhängt, bei 401 einmal erneuert und die wartenden Requests wiederholt — dazu, wo du das Token aufbewahrst.Sicherheit & Globalisierung
  • 17OAuth 2.1 & OpenID Connect mit PKCEDer Authorization-Code-Flow mit PKCE Schritt für Schritt: Verifier und Challenge, State und Nonce, das ID-Token und warum ein BFF sicherer ist.Sicherheit & Globalisierung
  • 17Internationalisierung (i18n)$localize, das i18n-Attribut, ICU plural/select, ein Build pro Sprache vs. Übersetzungen zur Laufzeit und Formatierung nach Locale.Sicherheit & Globalisierung