AngularAtlasAusg. 22.2

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

Monorepo, Bibliotheken & Grenzen

Ein Workspace mit mehreren Apps und Bibliotheken: ng generate library, Nx, Secondary Entry Points, Publishing und Abhängigkeitsregeln.

Architektur ~0 Min. Lesezeit Offizielle Doku

Ein Monorepo ist ein einziges Git-Repository, das mehrere Projekte enthält: zum Beispiel den Shop, das Admin-Panel und die Bibliotheken, die sie gemeinsam nutzen. Es ist kein Monolith: Jede Anwendung wird separat gebaut und deployt. Nur der Code liegt zusammen, mit einer Angular-Version und einer Commit-Historie.

Der Gewinn: Eine Änderung, die eine Bibliothek und die beiden Anwendungen betrifft, die sie nutzen, landet in einem einzigen PR und wird gemeinsam getestet. Der Preis: Ohne Tooling baut und testet jeder PR alles neu; ohne Regeln importiert jeder alles. Diese Seite behandelt beide Hälften: den Workspace mit der Angular CLI und ng-packagr, dann das, was Nx obendrauf bringt — den Projektgraphen, affected, den Cache und Boundaries, die beim Linting geprüft werden.

Kurz gesagt
  • Ein Monorepo hält mehrere Apps und Bibliotheken in einem Repository, mit einem einzigen Satz an Abhängigkeitsversionen.
  • Die Angular CLI unterstützt mehrere Projekte in einem Workspace von Haus aus (ng generate library). Nx ergänzt den Abhängigkeitsgraphen, Grenzen und Caching.
  • Eine Bibliothek hat eine öffentliche API: Was nicht aus ihrer Einstiegsdatei exportiert wird, bleibt intern.
  • „Affected“ führt Build und Tests nur für das aus, was eine Änderung berührt hat.
  • Meist reicht ein Ordner. Eine Bibliothek lohnt sich, wenn sie veröffentlicht oder von mehreren Apps genutzt wird.
Warum das wichtig ist

„Ordner, Bibliothek oder npm-Paket“ ist eine Architekturentscheidung, keine Stilfrage. Sie bestimmt, wie schnell du eine übergreifende Änderung ausliefern kannst und wie leicht du etwas kaputt machst, ohne es zu merken. Die Angular-Dokumentation sagt es direkt: Eine Bibliothek zwingt dich, Code von der Logik der Anwendung zu entkoppeln, kostet aber Wartungszeit — und das zahlt sich aus, wenn mehrere Anwendungen sie nutzen.

Monorepo, Polyrepo, Monolith

Die drei Modelle unterscheiden sich vor allem in den Kosten einer übergreifenden Änderung. Angenommen, das Modell Product bekommt ein neues Feld, das die UI-Bibliothek und beide Anwendungen verwenden:

ModellSo sieht es aus„Product“ ändernDer Preis
MonolithEine Anwendung, ein Deployment, Ordner nach Domäne1 PR, trivialTaugt nicht für zwei Anwendungen mit unterschiedlichem Release-Rhythmus
Polyrepo + npmJede Bibliothek im eigenen Repo, mit Version veröffentlicht3+ PRs: Bibliothek → Publish → Bump in jeder AnwendungVersionen driften auseinander; ein Fix erreicht Production erst, wenn jedes Team daran „denkt“
MonorepoAnwendungen + Bibliotheken im selben Repo, aus dem Workspace konsumiert1 atomarer PR, getestet mit allen KonsumentenDie CI muss clever sein (affected, Cache) und die Boundaries müssen durchgesetzt werden

Ein Angular-CLI-Workspace hat genau eine package.json im Root, also nutzen alle Projekte dieselbe Version von Angular, RxJS oder CDK — das nennt Nx die Single-Version-Policy. Ein ng update aktualisiert das ganze Repo auf einmal. Der Preis ist Koordination: Kein Team bleibt zurück, aber keines kann vorauslaufen. Wenn Teams wirklich unabhängig ausliefern müssen, mit unterschiedlichem Rhythmus und unterschiedlichen Versionen, ist der nächste Schritt Micro Frontends (die ironischerweise oft ebenfalls in einem Monorepo leben).

Multi-Projekt-Workspace mit der Angular CLI

ng new erstellt standardmäßig eine Anwendung im Root. Für ein Monorepo startest du mit einem leeren Workspace (--no-create-application), und jedes ng generate application oder ng generate library fügt ein Projekt unter projects/ und einen Eintrag in angular.json hinzu. Eine Bibliothek mit Scope (@acme/ui) landet in projects/acme/ui, und der Projektname ist der Paketname.

# 1. Leerer Workspace: keine Anwendung im Root (createApplication: false)
ng new acme --no-create-application
cd acme

# 2. Projekte landen in projects/ (newProjectRoot)
ng generate application shop
ng generate application admin
ng generate library @acme/ui          # → projects/acme/ui, Standard-Präfix "lib"

# 3. Jeder Befehl bekommt den Projektnamen
ng serve shop
ng build admin
ng test @acme/ui
ng build @acme/ui --watch             # inkrementeller Rebuild, während du an der Bibliothek arbeitest

Der wichtige Unterschied steckt in der angular.json: Anwendungen nutzen den Builder @angular/build:application (esbuild), Bibliotheken @angular/build:ng-packagr, der ein Paket im Angular Package Format erzeugt und immer AOT kompiliert. Für Bibliotheken ist die Standardkonfiguration production.

{
  "newProjectRoot": "projects",
  "projects": {
    "shop": {
      "projectType": "application",
      "root": "projects/shop",
      "sourceRoot": "projects/shop/src",
      "architect": {
        "build": { "builder": "@angular/build:application" },
        "serve": { "builder": "@angular/build:dev-server" }
      }
    },
    "@acme/ui": {
      "projectType": "library",
      "root": "projects/acme/ui",
      "sourceRoot": "projects/acme/ui/src",
      "prefix": "lib",
      "architect": {
        "build": {
          "builder": "@angular/build:ng-packagr",
          "defaultConfiguration": "production",
          "configurations": {
            "production": { "tsConfig": "projects/acme/ui/tsconfig.lib.prod.json" },
            "development": { "tsConfig": "projects/acme/ui/tsconfig.lib.json" }
          }
        }
      }
    }
  }
}
Namen sind wichtig, wenn du veröffentlichst

Vermeide das Präfix ng- (es ist dem Framework vorbehalten). Für öffentliche Pakete ist ngx- üblich; für interne ist ein Firmen-Scope (@acme/…) am klarsten und passt zu einer privaten Registry.

Aufbau einer Bibliothek

Vier Dateien definieren eine Bibliothek. Jede beantwortet eine bestimmte Frage:

  • ng-package.json — wohin der Build geht (dest) und welche Datei die öffentliche API enthält (lib.entryFile).
  • package.json — Name, Version und Abhängigkeiten. @angular/* gehören in peerDependencies: Stünden sie in dependencies, könnte die Anwendung eine zweite Kopie von Angular erhalten und zur Laufzeit kaputtgehen. "sideEffects": false sagt dem Bundler, dass er alles entfernen darf, was du nicht importierst.
  • public-api.ts — der Vertrag. Was hier nicht exportiert wird, ist privat, du kannst es also refaktorieren, ohne es jemandem zu sagen.
  • tsconfig.lib.prod.json — compilationMode: "partial": Das Partial-Ivy-Format ist über Angular-Versionen hinweg stabil, und die Kompilierung wird im Build der Anwendung mit deren eigener Compiler-Version abgeschlossen. Daher die Regel: Die Anwendung muss dieselbe Angular-Version verwenden, mit der die Bibliothek gebaut wurde, oder eine neuere.
{
  "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
  "dest": "../../../dist/acme/ui",
  "lib": {
    "entryFile": "src/public-api.ts"
  }
}

Secondary Entry Points

Das Angular Package Format erzeugt vor allem ein flaches ES-Modul pro Entry Point. Ein Bundler kann ein Modul nicht in mehrere Chunks aufteilen, daher ist der Entry Point die kleinste Granularität für Lazy Loading. Deshalb hat Angular Material einen Entry Point pro Komponente (@angular/material/button, …/tabs). Der zweite Grund: Du isolierst spezielle APIs wie @acme/ui/testing, damit sie nicht aus Versehen in Production landen.

projects/acme/ui/
├── ng-package.json          → @acme/ui             (primärer Entry Point)
├── src/public-api.ts
├── button/
│   ├── ng-package.json      → @acme/ui/button      (sekundärer Entry Point)
│   └── src/public-api.ts
└── testing/
    ├── ng-package.json      → @acme/ui/testing     (Harnesses, Mocks für Tests)
    └── src/public-api.ts

ng-packagr findet jede ng-package.json selbst; du registrierst nirgendwo etwas.

Eine Bibliothek mit nur einem Zweck (ein HTTP-Client, eine Sammlung von Hilfsfunktionen) kommt gut mit einem einzigen Entry Point aus — und @angular/core hat für die Runtime nur einen.

Konsum, Build und Veröffentlichung

Innerhalb desselben Workspace musst du nichts veröffentlichen, aber du musst die Bibliothek zuerst bauen. ng generate library fügt in der tsconfig.json einen Alias hinzu, der auf dist/ zeigt, nicht auf den Quellcode. Der Grund: Anwendungen und Bibliotheken haben unterschiedliche Builds (esbuild vs. ng-packagr), und dasselbe TypeScript kann als anderes JavaScript herauskommen. Die Dokumentation verlangt ausdrücklich, dass Aliase auf die gebaute Bibliothek zeigen. Während du arbeitest, baut ng build @acme/ui --watch inkrementell neu.

{
  "compilerOptions": {
    "paths": {
      // automatisch von ng generate library hinzugefügt — zeigt auf den BUILD, nicht auf den Quellcode
      "@acme/ui": ["./dist/acme/ui"],
      // von dir hinzugefügt, für die sekundären Entry Points
      "@acme/ui/*": ["./dist/acme/ui/*"]
    }
  }
}

Veröffentlichen heißt: Build in der Konfiguration production (der Standard) und npm publish aus dist — du veröffentlichst das generierte Paket, nicht den Quellordner.

# Distributions-Build: die Konfiguration production (Standard für Bibliotheken) → partial-Ivy
ng build @acme/ui

# Du veröffentlichst den INHALT von dist, nicht den Quellordner
cd dist/acme/ui
npm publish            # oder: npm publish --registry https://npm.company.com

# Der Konsument (anderes Repo) installiert es wie jedes andere Paket
npm install @acme/ui@^1.4.0
KonsumentWie er den Code bekommtVersionen
Anwendung im selben RepoEin Alias in der tsconfig, immer die neueste Version im BranchNicht nötig: Alle sind auf HEAD
Anderes Repo / anderer Clientnpm install @acme/ui aus der RegistryStriktes Semver, ein Changelog, korrekte peerDependencies
Nx macht es bewusst anders

Der Generator @nx/angular:lib setzt den Alias in die tsconfig.base.json und lässt ihn auf den Quellcode zeigen (libs/…/src/index.ts): Die Bibliothek wird zusammen mit der Anwendung kompiliert, ohne separaten Build-Schritt. Einen eigenen Build (--buildable / --publishable) fügst du nur für Bibliotheken hinzu, die du veröffentlichst, oder für inkrementelle Builds. Mische die beiden Stile nicht im selben Workspace, ohne zu wissen, warum.

Nx: Graph und Boundaries

Nx nutzt dieselben Angular-Builder, legt die Konfiguration jedes Projekts aber in einer eigenen project.json ab und baut aus der Analyse der Imports einen Projektgraphen. Alles Weitere stützt sich auf diesen Graphen: nx graph zeigt ihn dir, affected läuft ihn rückwärts ab, und die Lint-Regel prüft ihn bei jedem Import. In einem bestehenden Angular-CLI-Workspace startest du mit npx nx@latest init.

# Bestehender Angular-CLI-Workspace → Nx (behält die Struktur)
npx nx@latest init
# …oder verschiebe die Projekte nach apps/ und libs/
npx nx@latest init --integrated

# Bibliotheken mit Tags vom ersten Tag an
nx g @nx/angular:lib libs/shop/feature-cart --tags=scope:shop,type:feature
nx g @nx/angular:lib libs/shared/ui --tags=scope:shared,type:ui
nx g @nx/angular:lib libs/shared/ui --publishable --importPath=@acme/ui   # nur, wenn du sie veröffentlichst

# Der Projektgraph, interaktiv im Browser
nx graph
nx graph --focus shop-feature-cart

# Nur ausführen, was von den Änderungen gegenüber main betroffen ist
nx affected -t lint test build
nx affected -t build --graph        # den Task-Graphen ansehen, bevor du startest
nx run-many -t test                 # alles, ohne Filter

Bibliothekstypen

Der Nx-Leitfaden schlägt vier Typen vor — dieselben Rollen wie bei Smart vs. Dumb und in der Ordnerstruktur, nur dass die Grenze jetzt eine Bibliothek mit öffentlicher API ist:

TypWas er enthältDarf abhängen von
featureSeiten und Container-Komponenten für einen Use Case; meist lazyjedem Typ
uiPräsentations-Komponenten; die Daten kommen über Inputs, keine injizierten Servicesui, util
data-accessKommunikation mit dem Backend und Zustand (Stores)data-access, util
utilPure Funktionen, oft ohne Framework-Codeutil
acme/
├── nx.json                     ← cache, targetDefaults, defaultBase
├── tsconfig.base.json          ← "@acme/shop/feature-cart": ["libs/shop/feature-cart/src/index.ts"]
├── apps/
│   ├── shop/project.json       tags: scope:shop,   type:app
│   └── admin/project.json      tags: scope:admin,  type:app
└── libs/
    ├── shop/
    │   ├── feature-catalog/    tags: scope:shop,   type:feature      (Smart-Seiten, lazy)
    │   ├── feature-cart/       tags: scope:shop,   type:feature
    │   └── data-access/        tags: scope:shop,   type:data-access  (API + store)
    ├── admin/
    │   ├── feature-users/      tags: scope:admin,  type:feature
    │   └── data-access/        tags: scope:admin,  type:data-access
    └── shared/
        ├── ui/                 tags: scope:shared, type:ui           (Dumb-Komponenten)
        ├── data-access/        tags: scope:shared, type:data-access  (Auth, HTTP-Konfiguration)
        └── util/               tags: scope:shared, type:util         (pure Funktionen)

Tags in zwei Dimensionen

Jedes Projekt bekommt Tags in der project.json. In der Praxis funktioniert die Kombination aus Scope (die Domäne: scope:shop, scope:admin, scope:shared) × Typ (die Schicht: type:feature, type:ui…). Der Scope beantwortet „wer spricht mit wem“, der Typ „in welche Richtung fließen die Abhängigkeiten“.

{
  "name": "shop-feature-cart",
  "sourceRoot": "libs/shop/feature-cart/src",
  "projectType": "library",
  "tags": ["scope:shop", "type:feature"]
}

Die Regel @nx/enforce-module-boundaries

Die Lint-Regel nimmt eine Liste von depConstraints entgegen. Für einen Import nimmt Nx alle Constraints, deren sourceTag das Quellprojekt besitzt, und wendet sie mit UND an: shop/feature-cart muss sowohl die Scope-Regel als auch die Typ-Regel erfüllen. onlyDependOnLibsWithTags verlangt, dass das Ziel mindestens eines der Tags hat. Ein Projekt ohne passenden Tag darf von nichts abhängen.

import nx from '@nx/eslint-plugin';

export default [
  ...nx.configs['flat/base'],
  ...nx.configs['flat/typescript'],
  ...nx.configs['flat/javascript'],
  {
    files: ['**/*.ts'],
    rules: {
      '@nx/enforce-module-boundaries': [
        'error',
        {
          allow: [],
          depConstraints: [
            // Dimension 1: die Domäne (wer mit wem sprechen darf)
            { sourceTag: 'scope:shop', onlyDependOnLibsWithTags: ['scope:shop', 'scope:shared'] },
            { sourceTag: 'scope:admin', onlyDependOnLibsWithTags: ['scope:admin', 'scope:shared'] },
            { sourceTag: 'scope:shared', onlyDependOnLibsWithTags: ['scope:shared'] },
            // Dimension 2: der Typ (die Richtung, in die Abhängigkeiten fließen)
            { sourceTag: 'type:app', onlyDependOnLibsWithTags: ['type:feature', 'type:ui', 'type:data-access', 'type:util'] },
            { sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:feature', 'type:ui', 'type:data-access', 'type:util'] },
            { sourceTag: 'type:ui', onlyDependOnLibsWithTags: ['type:ui', 'type:util'] },
            { sourceTag: 'type:data-access', onlyDependOnLibsWithTags: ['type:data-access', 'type:util'] },
            { sourceTag: 'type:util', onlyDependOnLibsWithTags: ['type:util'] },
          ],
        },
      ],
    },
  },
];

Neben den Tags hat die Regel feste Prüfungen, die vor ihnen laufen. Drei davon zählen im Alltag, in dieser Reihenfolge: zirkuläre Abhängigkeiten, der Import einer Anwendung (verboten) und ein statischer Import einer lazy geladenen Bibliothek (er würde den Chunk ins Haupt-Bundle ziehen). Alle siehst du in der Demo:

LiveDer Workspace-Graph und die Boundary-Regeln (Simulation)
appsshopappsadminshopfeature-catalogshopfeature-cartadminfeature-usersshopdata-accessshareduiadmindata-accessshareddata-accesssharedutil

scope:shopscope:sharedscope:adminlazy (loadChildren)

scope:shop, type:feature → scope:admin, type:data-access
Probier:
Wähle Quelle und Ziel und drücke dann „Lint auf den Import ausführen“. Die Kante wird grün oder rot, und darunter siehst du die Meldung der Regel und jedes angewendete Constraint. Die Engine bildet die Prüfungen und Meldungen von @nx/enforce-module-boundaries in derselben Reihenfolge nach; sie führt nicht das echte ESLint aus.

Dieselbe Regel akzeptiert auch feinere Formen:

// Weitere nützliche Formen von Constraints (dieselbe Regel, dieselben depConstraints):
[
  // "ui" darf data-access auch nicht TRANSITIV erreichen (prüft die ganze Kette)
  { sourceTag: 'type:ui', notDependOnLibsWithTags: ['type:data-access'] },

  // die Regel gilt nur für Projekte, die BEIDE Tags haben
  { allSourceTags: ['scope:shop', 'type:ui'], onlyDependOnLibsWithTags: ['scope:shared'] },

  // für einen Projekttyp verbotene npm-Pakete
  { sourceTag: 'type:util', bannedExternalImports: ['@angular/*'] },

  // Glob und Regex in Tags
  { sourceTag: 'scope:*', onlyDependOnLibsWithTags: ['/^scope:(shared|core)$/'] },
]

Achte auf den Unterschied: notDependOnLibsWithTags prüft die gesamte Kette der Abhängigkeiten des Ziels, nicht nur die direkte. Das ist das richtige Werkzeug für „die UI erreicht nie data-access, auch nicht über Umwege“. Für Boundaries zwischen Ordnern in einer einzelnen Anwendung, ohne Nx, siehe Boundaries.

Affected und Cache

nx affected -t test startet bei den Dateien, die sich zwischen --base (standardmäßig defaultBase, also main) und --head geändert haben, ordnet sie Projekten zu und läuft dann im Graphen entlang der umgekehrten Kanten nach oben: Alles, was direkt oder transitiv von einem geänderten Projekt abhängt, ist betroffen. Im Demo-Workspace betrifft eine Änderung in shop/data-access 4 Projekte (die Bibliothek, die beiden Shop-Features und die Shop-Anwendung); eine in shared/util betrifft alle 10.

Dazu kommt der Cache: Bevor ein Task läuft, berechnet Nx einen Hash seiner Inputs — die Quellen des Projekts, die seiner Abhängigkeiten (gemäß inputs), die Konfiguration, das Kommando. Wurde der Hash schon einmal gesehen, spielt Nx das Ergebnis erneut ab (die Terminal-Ausgabe und die Dateien), statt den Task auszuführen. Mit einem Remote-Cache läuft ein Task, der einmal in der CI gelaufen ist, auf keinem Laptop mehr.

Livenx affected -t test, mit Cache (Simulation)
appsshopappsadminshopfeature-catalogshopfeature-cartadminfeature-usersshopdata-accessshareduiadmindata-accessshareddata-accesssharedutil

geändert ✎betroffengelaufen ✓aus dem Cache

0/10
Projekte betroffen
0s
Test auf affected (vs 3m 51s alle)

Klicke auf ein Projekt im Graphen: Du änderst eine seiner Dateien.

  • Hier erscheint das CI-Terminal.
Klicke auf shared/util: Fast das ganze Repo ist betroffen. Führe es aus und dann noch einmal — beim zweiten Mal kommt alles aus dem Cache. Ändere dann shop/feature-cart und führe aus: Nur feature-cart und shop laufen, der Rest ist ein Cache-Hit. „Merge in main“ verschiebt die Base; „nx reset“ leert den Cache.
{
  "defaultBase": "main",
  "namedInputs": {
    "default": ["{projectRoot}/**/*"],
    "production": ["default", "!{projectRoot}/**/*.spec.ts"]
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production"],
      "cache": true
    },
    "lint": { "cache": true }
  }
}
Granularität macht affected präzise

Wenn jede Anwendung von einer riesigen shared-Bibliothek abhängt, betrifft jede Änderung darin das ganze Repo, und affected spart nichts mehr. Kleinere Bibliotheken, nach Domäne und Typ aufgeteilt, bedeuten engere Graphen und mehr Cache-Hits.

Der Cache setzt Tasks ohne Seiteneffekte voraus: Dieselben Inputs liefern dasselbe Ergebnis. Ein E2E-Test, der ein echtes Backend anspricht, ist nicht cachebar — sein Ergebnis hängt von etwas ab, das Nx nicht sehen kann.

Ordner oder Bibliothek?

Der Nx-Leitfaden zur Projektgröße fasst den Kompromiss gut zusammen. Du teilst in Bibliotheken auf, wenn Tests dich ausbremsen (affected und Cache beginnen, Teile zu überspringen), wenn der Code einen anderen Besitzer hat (ein Team, eine CODEOWNERS-Datei) oder wenn du verifizierte Boundaries willst. Was sich immer gemeinsam ändert, teilst du nicht auf — die Grenze kostet Navigation und Konfiguration, ohne etwas zu bringen. Und solange das Design noch erkundet wird, ist ein einziges Projekt billiger; die Bibliothek extrahierst du, sobald die API sich gesetzt hat.

LiveOrdner, interne Bibliothek oder veröffentlichbares Paket?
Score: -3
Ein Ordner reicht

Ein Ordner im Feature (oder in shared/) mit einer index.ts als öffentlicher API bringt dir 80 % der Vorteile ohne Konfiguration. Extrahiere die Bibliothek, sobald ein zweiter Konsument auftaucht.

src/app/features/cart/ui/ + Lint-Regel für Imports
Hake an, was für deinen Code zutrifft. Der Score ist eine Heuristik auf Basis des Nx-Leitfadens zur Projektgröße, kein Gesetz. Dieselbe Frage auf Ordnerebene: die Seite Shared vs. Feature.

Für die Option „Ordner“ bringen dir die Regeln aus Shared vs. Feature und eine index.ts als öffentliche API die meisten Vorteile, ganz ohne zusätzliche Konfiguration.

Wann ja, wann nein

Monorepo mit Bibliotheken
  • Zwei oder mehr Anwendungen teilen Domänencode oder ein internes Design-System.
  • Häufige übergreifende Änderungen (API-Modelle, Auth, Theme), die du atomar in einem PR ausliefern willst.
  • Mehrere Teams im selben Code: Tags und die Lint-Regel machen die Boundaries explizit.
  • Eine langsame CI: affected + Cache verkürzen die Zeit proportional zu dem, was du angefasst hast.
  • Ein Design-System, das auch von außerhalb des Repos genutzt wird: eine veröffentlichbare Bibliothek, gebaut mit ng-packagr.
Bei einer Anwendung mit Ordnern bleiben
  • Eine Anwendung, ein Team: Domänenordner (Ordnerstruktur) und eine no-restricted-imports-Regel geben dir dieselben Boundaries.
  • Teams, die völlig unabhängig ausliefern müssen, auch mit unterschiedlichen Angular-Versionen: Polyrepo oder Micro Frontends.
  • Eine Bibliothek pro Komponente: Konfiguration ohne Nutzen. Eine Bibliothek pro Domäne und Typ, nicht pro Datei.
  • Veröffentlichung auf npm für Code, der nur im Repo verwendet wird: Versionen bringen Zeremonie ohne Wert.

Häufige Stolperfallen

Ein Alias auf den Quellcode der Bibliothek in einem Angular-CLI-Workspace

"@acme/ui": ["projects/acme/ui/src/public-api.ts"] wirkt bequem (du musst die Bibliothek nicht mehr bauen), aber die Anwendung kompiliert den Code mit esbuild, während externe Konsumenten das bekommen, was ng-packagr erzeugt. Die Bugs treten nur bei ihnen auf. Fix: ein Alias auf dist und ng build @acme/ui --watch — oder wechsle zu Nx, das dafür gedacht ist, aus dem Quellcode zu kompilieren.

@angular/core in dependencies

Die Bibliothek bringt ihre eigene Kopie von Angular mit, und die Anwendung endet mit zwei Instanzen: DI und Change Detection gehen auf schwer zu diagnostizierende Weise kaputt. Fix: Alles, was @angular/* ist, gehört in peerDependencies.

  • Deep Imports (@acme/ui/src/lib/button/button), die public-api.ts umgehen: Das erste interne Refactoring bricht die Konsumenten. Fix: aus der öffentlichen API exportieren oder einen Secondary Entry Point anlegen; das Muster mit no-restricted-imports verbieten.
  • Relative Imports zwischen Entry Points derselben Bibliothek: ng-packagr baut jeden Entry Point separat, und der Build schlägt fehl. Fix: über den Paketnamen importieren (@acme/ui).
  • Projekte ohne Tags, „repariert“ mit sourceTag: '*' → '*': Die Regel prüft nichts mehr. Fix: --tags beim Generieren und die Tags im Review prüfen.
  • Eine Shared-Bibliothek als Müllschublade, von der alles abhängt: affected = das ganze Repo bei jedem PR. Fix: nach Domäne und Typ aufteilen.
  • Nicht deterministische Tasks cachen (E2E gegen ein echtes Backend, Tests, die die Uhrzeit lesen): Nx spielt ein Ergebnis ab, das nicht mehr stimmt. Fix: kein cache: true bei diesen Targets, oder vollständige Inputs.
  • Ein statischer Import über einem loadChildren: Der Lazy-Chunk verschwindet im initialen Bundle. Die Nx-Regel erkennt das; ohne Nx kontrollierst du das Bundle (siehe Performance).

Spickzettel

ANGULAR CLI (Multi-Projekt)
  ng new acme --no-create-application        leerer Workspace
  ng g application shop | ng g library @acme/ui
  ng build @acme/ui [--watch]                 Bibliotheken werden gebaut, BEVOR sie konsumiert werden
  tsconfig paths → ./dist/acme/ui             (+ "@acme/ui/*" für sekundäre Entry Points)
  cd dist/acme/ui && npm publish              nur was das Repo verlässt, wird veröffentlicht

BIBLIOTHEK
  ng-package.json        dest + lib.entryFile          (sekundär: nur "lib")
  public-api.ts          die öffentliche API — der Rest ist privat
  package.json           peerDependencies für @angular/*, "sideEffects": false
  compilationMode        "partial" für npm

NX
  nx graph                         der Projektgraph
  nx affected -t lint test build   nur die betroffenen Projekte (inkl. transitiver Abhängiger)
  "cache": true in targetDefaults  Hash über Inputs → Replay beim zweiten Lauf
  tags + @nx/enforce-module-boundaries → Boundaries werden beim Linting durchgesetzt
  Typen: feature → ui, data-access, util · ui → ui, util · data-access → data-access, util · util → util

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