Frontend-Lernpfad: Angular an diesem Projekt lernen
Zielgruppe: Entwickler:in, die Angular an einem echten, kleinen, aber vollständigen Codebestand nachvollziehen will - nicht an einem Tutorial-Wegwerfprojekt. Jede Etappe verweist auf konkrete Dateien unter
library-frontend/src/app/, die du parallel offen haben solltest.Kommst du aus einer alten Angular-Version (v8 o. ä.) und ist vieles verblasst? Lies zuerst den Exkurs: Angular 8 → 22 - der ordnet die Änderungen von 14 Major-Releases nach Konzept und sagt dir, welche deiner alten Reflexe noch tragen.
Warum dieses Frontend zum Lernen taugt
Die SPA ist bewusst klein (rund 25 TypeScript-Dateien) und ohne "Magie": keine selbstgebaute Abstraktionsschicht, keine Framework-im-Framework-Konstruktion. Dafür steckt in fast jeder Datei eine bewusste Design-Entscheidung mit einem sichtbaren Gegenstück im selben Projekt:
- Signals als Standard-Zustandsverwaltung - NgRx in genau einem Feature
(
catalog-admin), damit du beide Ansätze direkt nebeneinander siehst. - Standalone Components (kein einziges
NgModule). - Funktionale Guards und HTTP-Interceptors (Angular 15+) statt der älteren klassenbasierten Schnittstellen.
- Reactive Forms überall - nie Template-driven Forms.
- OIDC Authorization Code Flow + PKCE gegen Keycloak - dieselbe Auth-Mechanik wie im Backend, nur clientseitig.
Die fachliche Domäne (Bücher, Ausleihen, Mitglieder, Mahngebühren) ist Nebensache - sie ist nur der Aufhänger, an dem die Angular-Konzepte hängen.
Version: Angular 22 (package.json: @angular/core ^22.1), TypeScript ~6.0, zonen-basierte
Change Detection (provideZoneChangeDetection, nicht zoneless). In zwei Etappen von Angular
18 hochgezogen (18→20, dann 20→22) - der Weg dorthin und die Regeln für Major-Updates allgemein
stehen in angular-update-migration.md.
Voraussetzungen
- Frontend läuft:
cd library-frontend npm install npx ng serve # http://localhost:4200, Hot-Reload - Backend + Keycloak laufen (sonst kommst du nicht über den Login hinaus):
Testnutzer stehen incd infra podman compose up -d # mindestens: keycloak, api-gateway, catalog-service, member-serviceinfra/keycloak/realm-library.json(z. B.lisa.bibliothekarin/librarian123mit RolleLIBRARIAN). - Angular DevTools (Browser-Extension für Chrome/Firefox) - zeigt den Component-Baum, die Signal-Werte und (mit Redux DevTools zusätzlich) den NgRx-Store live.
Wenn das Backend gerade nicht läuft: Die Etappen 0-1 und 5-7 kannst du rein am Code nachvollziehen. Für 2-4 brauchst du echte API-Antworten.
Konventionen dieses Codebestands (einmal lesen, dann wiedererkennst du sie überall)
| Konvention | Beispiel | Warum |
|---|---|---|
inject() statt Konstruktor-Parameter |
private readonly http = inject(HttpClient); |
Kürzer, funktioniert auch außerhalb des Konstruktors (funktionale Guards/Interceptors), gleiche DI wie im Backend-Konstruktor-Injection-Prinzip |
protected readonly für alles, was das Template nutzt |
protected readonly catalogService = ... |
strictTemplates (AOT) prüft Template-Zugriffe; protected macht klar "nur fürs eigene Template", private würde das Template nicht sehen |
Ein *.service.ts pro Feature hält den Zustand |
CatalogService.books = signal([]) |
Die Komponente bleibt dünn, der Zustand überlebt einen Komponenten-Neuaufbau (Service ist providedIn: 'root') |
| Fehler zentral aus RFC-7807 lesen | extractErrorMessage(err) in jedem Service |
Das Backend liefert ProblemDetail (err.error.detail) - dieselbe Fehlerkontrakt-Annahme auf beiden Seiten, siehe common-web.GlobalExceptionHandler |
shared/models/*.model.ts spiegelt Backend-DTOs 1:1 |
BookResponse ⟷ catalog-service.api.dto.BookResponse |
Kein eigenes Frontend-Datenmodell, keine Übersetzungsschicht |
| Neue Control-Flow-Syntax | @if, @else if, @for in Templates |
Angular 17+ - ersetzt *ngIf/*ngFor, kein CommonModule-Import mehr nötig |
Etappe 0 - Wie die App startet
Dateien: src/main.ts, src/app/app.config.ts, src/app/app.component.ts + .html
Schau dir an:
main.tsist fünf Zeilen:bootstrapApplication(AppComponent, appConfig). KeinAppModule. Das ist der Standalone-Bootstrap (Angular 14+, ab 17 der Default ausng new).app.config.tsist die zentrale Provider-Liste - das, was früher in@NgModule({ providers, imports })stand. JedeprovideXxx()-Funktion schaltet eine Framework-Fähigkeit frei:provideRouter,provideHttpClient,provideAnimations,provideStore(NgRx).app.component.tsist die Shell: Toolbar +<router-outlet />.app.component.htmlnutzt@if (authService.isLoggedIn())um zwischen der App und einer Login-Landingpage umzuschalten.
Versteh warum:
Der provideAppInitializer(...)-Aufruf am Ende von app.config.ts ist der wichtigste Teil.
Angular hält den Bootstrap an, bis das von authService.initialize() zurückgegebene Promise
auflöst. Erst danach darf der Router die erste Route aktivieren. Ohne diese Sperre würde ein
Guard über einen Login-Zustand entscheiden, der noch gar nicht aus Keycloak geladen wurde.
(provideAppInitializer löste beim Update auf v19+ den älteren APP_INITIALIZER-Multi-Provider
ab - siehe angular-update-migration.md.)
Mini-Übung: Öffne die Angular DevTools, geh auf den Component-Baum. Du siehst genau eine
Komponente (AppComponent), solange du ausgeloggt bist. Logg dich ein - jetzt erscheint darunter
die lazy geladene Feature-Komponente.
Etappe 1 - Routing und Lazy Loading
Datei: src/app/app.routes.ts
Schau dir an:
- Jede Route nutzt
loadComponent: () => import('...').then(m => m.XyzComponent). Das erzeugt pro Feature ein eigenes JS-Chunk, das erst beim Navigieren dorthin geladen wird. Prüf das:npx ng buildund schau indist/- du bekommst mehrerechunk-*.js. - Bei Standalone Components reicht dafür eine dynamische
import()auf die Komponente. Der klassische Weg warloadChildrenauf ein ganzes Feature-NgModulemit eigenerRouterModule.forChild(...)-Routentabelle. canActivate: [authGuard]undcanActivate: [authGuard, roleGuard([...])]- Guards als Array, in Reihenfolge ausgewertet.- Die letzten beiden Einträge:
{ path: '', redirectTo: 'catalog' }und{ path: '**', redirectTo: 'catalog' }- Default-Route und Wildcard-Fallback. title: 'Katalog durchsuchen'- Angular setzt daraus automatisch den<title>des Tabs (provideRouterbringt denTitleStrategy-Service mit).
Versteh warum: Lazy Loading verkürzt die initiale Ladezeit - der Nutzer lädt nur den Code für die Seite, die er tatsächlich öffnet. Bei sechs Features ist das spürbar.
Mini-Übung: Öffne die Netzwerk-Ansicht der Browser-DevTools, lade /catalog neu, klick dann
auf "Vormerkungen". Beim Klick siehst du ein neues chunk-*.js nachgeladen werden.
Etappe 2 - Ein Feature von vorne bis hinten: catalog
Dateien: src/app/features/catalog/ (catalog-search.component.ts, .html, catalog.service.ts)
src/app/shared/models/catalog.model.ts
Das ist das einfachste vollständige Feature - lies es als Blaupause für alle anderen (außer
catalog-admin).
Schau dir an:
catalog.model.ts- reine TypeScript-interfaces.BookResponse,PageResponse<T>. Der Kommentar sagt: spiegeltcatalog-service.api.dto.BookResponse(Java). Keine Klasse, kein Decorator - nur die Form der JSON-Antwort.catalog.service.ts-@Injectable({ providedIn: 'root' }). Hält drei Signals:books,loading,error. Die Methodesearch(criteria)bautHttpParams, ruftthis.http.get<PageResponse<BookResponse>>(...)auf und schreibt imsubscribe-Callback die Signals.catalog-search.component.ts- injiziert den Service, baut einsearchForm(Reactive Form), ruft im Konstruktor einmalsearch()für die initiale Anzeige.catalog-search.component.html-[formGroup]="searchForm",(ngSubmit)="search()", und für die Ergebnisliste:
Beachte:@if (catalogService.loading()) { <mat-spinner /> } @else if (catalogService.books().length === 0) { <p>Keine Bücher gefunden.</p> } @else { <table mat-table [dataSource]="catalogService.books()"> ... </table> }catalogService.books()- die runden Klammern lesen den Signal-Wert. Im Template löst dieser Lesezugriff automatisch die Neuberechnung aus, wenn sich das Signal ändert.
Versteh warum: Die Komponente enthält keine Logik außer "Formularwerte an den Service geben". Der gesamte Zustand (Ladezustand, Fehler, Ergebnisse) lebt im Service. Zwei Vorteile: der Zustand überlebt das Verlassen und Wiederbetreten der Seite, und der Service ist ohne DOM testbar.
Mini-Übung: Füge dem searchForm ein Feld category: '' hinzu, reich es in search() als
weiteren HttpParams-Eintrag durch, und bau im Template ein <mat-form-field> dafür. Du fasst
dabei Komponente, Service und Template an - der typische Dreiklang jeder Änderung hier.
Etappe 3 - Zustand mit Signals
Dateien: alle *.service.ts unter features/ außer catalog-admin/
Schau dir an: Das Muster ist in jedem Service identisch:
readonly items = signal<Xyz[]>([]);
readonly loading = signal(false);
readonly error = signal<string | null>(null);
load(...): void {
this.loading.set(true);
this.http.get<...>(...).subscribe({
next: (data) => { this.items.set(data); this.loading.set(false); },
error: (err) => { this.error.set(this.extractErrorMessage(err)); this.loading.set(false); },
});
}
signal(x)erzeugt einen schreibbaren Signal..set(v)überschreibt,.update(fn)leitet vom alten Wert ab.- Die Komponente liest ihn im Template als
service.items(). Keinasync-Pipe, kein manuellessubscribe/unsubscribe, keinOnDestroy. - Schreibaktionen (
borrow,pay,suspend, ...) nehmen einenonSuccess-Callback statt selbst ein Signal zu pflegen - sieheloans.service.ts. Die Komponente entscheidet, was nach Erfolg passiert (meist: Liste neu laden).
Versteh warum: Für "lade eine Liste, zeig einen Spinner, zeig einen Fehler" ist ein Signal-
Trio die kleinstmögliche Lösung. Kein Boilerplate. Der Preis: die Zustandsänderungen sind
verstreut (jedes .set() ist eine eigene Stelle), es gibt kein Protokoll, kein Time-Travel-
Debugging. Genau hier setzt Etappe 7 an.
Mini-Übung: Öffne die Angular DevTools, wähle eine Feature-Komponente, sieh dir im
"Properties"-Tab den injizierten Service und seine Signal-Werte an. Führe eine Suche aus und
beobachte, wie loading von true auf false springt.
Etappe 4 - Reactive Forms
Dateien: catalog-search.component.ts (einfach), members-admin.component.ts (mehrere
Formulare, typisierte Werte, Validators)
Schau dir an:
this.formBuilder.nonNullable.group({ title: '', author: '', onlyAvailable: false })- dernonNullable-Builder sorgt dafür, dassreset()auf die Initialwerte zurücksetzt statt aufnull, und dass die Typen nichtstring | nullsind.members-admin.component.tszeigt Validators:['', Validators.required],['', [Validators.required, Validators.email]],[1, [Validators.required, Validators.min(1)]].- Typisierte Auswahlwerte:
tier: ['STANDARD' as (typeof this.tiers)[number], Validators.required]- der Formularwert ist auf
'STANDARD' | 'PREMIUM' | 'STUDENT'eingeschränkt.
- der Formularwert ist auf
- Im Template:
[formGroup],formControlName,(ngSubmit), und[disabled]="form.invalid"am Submit-Button. form.getRawValue()liest alle Werte (inklusive deaktivierter Controls) als getipptes Objekt.
Versteh warum: Der Formularzustand lebt im TypeScript-Code, nicht im Template. Du kannst ihn
in einem Unit-Test ohne gerendertes DOM prüfen. Template-driven Forms ([(ngModel)]) wären
kürzer zu tippen, aber der Zustand wäre implizit im DOM verstreut.
Mini-Übung: Der email-Validator zeigt im Template noch keine Fehlermeldung an. Ergänze
@if (registerForm.controls.email.hasError('email')) { <mat-error>Keine gültige E-Mail</mat-error> }.
Etappe 5 - Login: OIDC Authorization Code Flow + PKCE
Dateien: src/app/core/auth/auth.config.ts, auth.service.ts, und der
provideAppInitializer(...)-Aufruf in app.config.ts
Schau dir an:
auth.config.ts- die OIDC-Parameter:issuer(Keycloak-Realm-URL),clientId: 'library-frontend',responseType: 'code',scope: 'openid profile email'. KeinclientSecret- eine Browser-App ist ein "public client", ein Secret im JS-Bundle wäre nicht geheim. PKCE (Proof Key for Code Exchange) übernimmt dessen Schutzfunktion.auth.service.ts- ein dünner Wrapper umOAuthServiceausangular-oauth2-oidc. Er gibt den Login-Zustand als Signals nach außen:isLoggedIn,roles,username.initialize()konfiguriertOAuthService, abonniert dessenevents(Login/Logout/Refresh), ruftsetupAutomaticSilentRefresh()(holt neue Tokens vor Ablauf -accessTokenLifespanist bewusst kurz, siehe ADR-0004) undloadDiscoveryDocumentAndTryLogin().extractRealmRoles(accessToken)dekodiert das JWT selbst (atob(payload)) und liestrealm_access.roles- exakt derselbe Claim-Pfad wie im Backend (common-security.KeycloakRealmRoleConverter). Rollen kommen aus dem Access-Token, nicht aus dem ID-Token.
login()ruftinitCodeFlow()- das leitet den Browser zu Keycloak um.app.config.ts-provideAppInitializer(() => inject(AuthService).initialize())hält den Bootstrap an, bisinitialize()fertig ist (Angular 19+ - vorherAPP_INITIALIZER).
Versteh warum: Der Flow ist: App → Keycloak-Login-Seite → zurück mit ?code=... → App tauscht
Code (+ PKCE-Verifier) gegen Access-Token + ID-Token → Token liegt im Speicher, wird bei jedem
API-Call mitgeschickt (Etappe 6).
Mini-Übung: Log dich ein, öffne die Browser-DevTools → Application → Session Storage. Du
findest access_token und id_token. Kopiere das Access-Token in https://jwt.io und finde den
realm_access.roles-Claim - das ist genau, was auth.service.ts ausliest.
Etappe 6 - HTTP-Interceptor und Route Guards (funktional)
Dateien: src/app/core/auth/auth.interceptor.ts, src/app/core/auth/role.guard.ts
Schau dir an:
authInterceptorist eine Funktion vom TypHttpInterceptorFn, registriert überprovideHttpClient(withInterceptors([authInterceptor])). Sie:- lässt Requests durch, die nicht an
API_BASE_URLgehen (z. B. Keycloak selbst), - hängt sonst
Authorization: Bearer <token>an viarequest.clone({ setHeaders: {...} }).
HttpRequestist immutable - deshalb.clone(), kein direktes Setzen.- Der klassische Weg war eine
@Injectable-Klasseimplements HttpInterceptormitintercept(req, next), registriert über denHTTP_INTERCEPTORS-Multi-Token.
- lässt Requests durch, die nicht an
roleGuard(requiredRoles: string[])ist eine Guard-Factory: sie gibt eineCanActivateFnzurück. So kannst du inapp.routes.tsroleGuard(['LIBRARY_ADMIN', 'LIBRARIAN'])schreiben, statt für jede Rollenkombination eine eigene Guard-Klasse zu bauen.authGuardist direkt eineCanActivateFn. Sie leitet bewusst nicht selbst zu Keycloak um - siehe das Javadoc in der Datei: die Shell zeigt sonst nie ihre "Bitte melde dich an"-Seite.
Versteh warum (wichtig für Reviewer:innen): Der roleGuard ist eine UX-Maßnahme, keine
Sicherheitsgrenze. Er blendet Menüpunkte aus. Die echte Autorisierung erzwingen ausschließlich
die Backend-Services per @PreAuthorize (common-security). Ein Frontend-Guard ist mit den
Browser-DevTools trivial zu umgehen.
Mini-Übung: Log dich als einfaches Mitglied ein (kein LIBRARIAN). Die Menüpunkte "Bestand
verwalten" / "Mitglieder" fehlen. Tippe die URL /admin/books trotzdem von Hand ein - der
roleGuard wirft dich auf /. Öffne dann das Backend-Log und ruf denselben Endpunkt direkt auf
(curl ohne passende Rolle) - 403 vom Server. Zwei Ebenen, nur die zweite zählt.
Etappe 7 - Der Kontrast: NgRx in catalog-admin
Dateien: src/app/features/catalog-admin/store/ (*.actions.ts, *.reducer.ts,
*.effects.ts, *.selectors.ts, *.state.ts) + catalog-admin.component.ts
Dieses eine Feature nutzt statt eines Signal-Services den vollen NgRx-Redux-Stack. Es tut
fachlich fast dasselbe wie catalog.service.ts (Bücher laden, Buch anlegen, Exemplar hinzufügen)
- der Vergleich ist der eigentliche Lerninhalt.
Schau dir an:
catalog-admin.state.ts-interface CatalogAdminState { books; loading; error }+ eininitialCatalogAdminState. Identisch zum Signal-Trio, nur als ein Objekt.catalog-admin.actions.ts-createActionGroupmit Ereignissen wieLoad Books,Load Books Success,Load Books Failure. Jede Interaktion und jeder Ausgang wird zu einer benannten Aktion.catalog-admin.reducer.ts-createReducer+on(Action, (state, props) => newState). Reine Funktionen, keinthis, kein HTTP - nur alter Zustand → neuer Zustand.catalog-admin.effects.ts- hier lebt der HTTP-Aufruf.createEffecthört aufofType(loadBooks), machtthis.http.get(...), mappt das Ergebnis aufloadBooksSuccessoder (viacatchError)loadBooksFailure. BeachtereloadAfterRegister$: ein Effect, der aufregisterBookSuccesshört undloadBooksneu auslöst - Verkettung über Aktionen.catalog-admin.selectors.ts-createFeatureSelector+createSelectorextrahieren Teilbäume des Stores.catalog-admin.component.ts- injiziertStore, liest überthis.store.selectSignal(selectCatalogAdminBooks)(NgRx gibt dir einen Signal zurück!) und schreibt nur überthis.store.dispatch(CatalogAdminActions.loadBooks({})). Die Komponente ist bewusst "dumm".- Registrierung in
app.config.ts:provideStore({ catalogAdmin: catalogAdminReducer }),provideEffects([CatalogAdminEffects]),provideStoreDevtools(...).
Versteh warum / wann sich das lohnt: Der detaillierte Zeile-für-Zeile-Vergleich steht in signals-vs-ngrx.md. Kurzfassung: NgRx kostet vier zusätzliche Dateien und viel Zeremonie, gibt dir dafür ein durchsuchbares Aktionsprotokoll (Redux DevTools), erzwungene Trennung von "was passiert ist" und "wie der Zustand darauf reagiert", und einen klaren Ort für Seiteneffekt-Verkettung. Für eine simple Ladeliste ist das Overkill - für einen komplexen Workflow mit vielen voneinander abhängigen Zustandsübergängen zahlt es sich aus.
Mini-Übung: Installiere die Redux DevTools-Extension. Öffne /admin/books, leg ein Buch an.
Im DevTools-Panel siehst du die Aktionssequenz: [Catalog Admin] Register Book →
[Catalog Admin] Register Book Success → [Catalog Admin] Load Books →
[Catalog Admin] Load Books Success. Klick auf eine Aktion und sieh den Zustand davor/danach.
Das gibt es bei den Signal-Services nicht.
Etappe 8 - Tests
Dateien: src/app/app.component.spec.ts, e2e/golden-path.spec.ts, e2e/README.md
Schau dir an:
app.component.spec.tstestet die Shell-Komponente mit Karma/Jasmine. Kernidee: der echteAuthServicewürde im Konstruktor ein Keycloak-Discovery-Dokument laden - deshalb registriert der Spec über{ provide: AuthService, useValue: new FakeAuthService() }ein Test-Double, das nur die nach außen gegebenen Signals (isLoggedIn,roles,username) undhasAnyRole/login/logoutnachbildet. Die einzelnen Tests setzenauth.isLoggedIn.set(true)bzw.auth.roles.set(['LIBRARIAN'])und prüfen, was die Komponente daraufhin rendert (Login-Landingpage vs.<router-outlet>, sichtbare/versteckte Admin-Links).login()/logout()sindjasmine.createSpy- so lässt sich der Button-Klick verifizieren.npx ng test --watch=false --browsers=ChromeHeadless # -> TOTAL: 8 SUCCESSe2e/golden-path.spec.ts(Playwright) fährt gegen den echten Stack: Login als Bibliothekarin → Mitglied anlegen → Buch anlegen → im Katalog finden → ausleihen → in "Meine Ausleihen" prüfen. Ein Durchlauf übt Signals-Features, das NgRx-Feature und den PKCE-Login zusammen.e2e/README.md#gefundene-bugslistet reale Bugs, die dieser Test aufgedeckt hat (u. a. der MapStruct-Fluent-Accessor-Bug, siehe Pattern-Katalog).- Bekannte lokale Hürde:
ng testbraucht ein installiertes Chrome für denChromeHeadless-Launcher (siehe Teststrategie, Abschnitt Frontend-Tests).
Versteh warum: Die Unit-Test-Ebene des Frontends ist bewusst dünn (eine Shell-Spec) - der Fokus lag auf einem belastbaren E2E-Golden-Path gegen den realen Stack statt auf vielen isolierten Komponenten-Specs. Die Shell-Spec zeigt das Muster (Service-Double + Signal setzen + gerendertes DOM prüfen), an dem sich Feature-Specs orientieren können.
Vertiefungsaufgaben (größere Brocken, in aufsteigender Schwierigkeit)
Eine Feature-Spec schreiben.
app.component.spec.tszeigt das Muster (Service-Double, Signal setzen, gerendertes DOM prüfen). Schreib nach demselben Schema einen Spec fürcatalog-search.component.ts: mit einemFakeCatalogService, dessenbooks/loading-Signals du im Test setzt, prüfst du "zeigt Spinner beiloading()", "zeigt Keine Bücher gefunden bei leerer Liste", "der Submit-Button ruftcatalogService.search(...)".Fehlermeldungen im Template. Kein Feature zeigt derzeit feldbezogene Validierungsfehler an (nur den globalen
error()-Banner). Ergänze<mat-error>-Blöcke für die Pflichtfeld-/E-Mail-Validators inmembers-adminundcatalog-admin.Ein Feature von Signals nach NgRx portieren (oder umgekehrt:
catalog-adminnach Signals). Nimmfines- es ist klein. Du baustfines.actions.ts,.reducer.ts,.effects.ts,.selectors.ts,.state.ts, registrierst sie inapp.config.tsund machst die Komponente "dumm". Danach hast du beide Varianten desselben Features gebaut und weißt aus erster Hand, was der Mehraufwand ist.Keycloak-Nutzer ↔ Member-Id verknüpfen. Aktuell muss ein Mitglied seine Member-UUID von Hand eintippen (siehe Javadoc in
my-loans.component.ts). Baue: einkeycloakUserId-Feld am Member-Aggregat (Backend), einen Lookup-EndpunktGET /api/members/by-keycloak-id/{sub}, und im Frontend einenMemberContextService, der beim Login densub-Claim nimmt, den Member auflöst und als Signal bereitstellt. Dann wird aus "Meine Ausleihen" tatsächlich meine.@deferausprobieren. Die Ergebnistabellen könnten mit dem Angular-17+-@defer-Block verzögert geladen werden (@defer (on viewport) { <table>... } @placeholder { ... }). Miss mit den DevTools, ob es beim initialen Rendern etwas bringt.
Weiterführend
- angular-konzepte.md - dieselben Konzepte als kompaktes Nachschlagewerk mit Gegenüberstellung "dieses Projekt vs. klassischer Angular-Weg".
- signals-vs-ngrx.md - der Zeile-für-Zeile-Vergleich der beiden Zustandsansätze.
- angular-update-migration.md - warum und wie man Angular-Majors aktualisiert; der durchgeführte Sprung 18 → 22 mit allen Stolpersteinen.
- exkurs-von-angular-8-nach-22.md - für Rückkehrer:innen aus einer alten Version: was sich seit Angular 8 konzeptionell geändert hat, mit Hands-on-Übungen.
- exkurs-von-angular-8-nach-18.md - dieselbe Aufbereitung mit Ziel Angular 18 (für ein Projekt, das noch auf v18 steht).
- Pattern-Katalog - die Frontend-Patterns mit Code-Fundstelle und Begründung (Functional Interceptor / Token Relay, Guard-Factory, PKCE, RFC-7807-Spiegel, Signals vs. NgRx).
- Tech-Stack-Katalog - warum Angular, warum NgRx nur punktuell.
- Teststrategie - Frontend-Testabschnitt.
library-frontend/e2e/README.md- der E2E-Golden-Path und die Bugs, die er fand.- Offizielle Angular-Doku: https://angular.dev/overview - besonders "Signals", "Standalone components", "Router", "HTTP client", "Forms".