60 Fragen, die 2026 wirklich in Angular-Interviews vorkommen, nach Level sortiert: was ein Junior wissen muss, was von einem Mid erwartet wird, der Features allein baut, und wonach ein Senior zu Internals, Architektur und Trade-offs gefragt wird. Jede Antwort beginnt mit dem, was du zuerst sagst, dann folgt, was du ergänzt, um dein Verständnis zu zeigen, die Frage, die meist danach kommt, und die Antwort, mit der man im Interview durchfällt.
So nutzt du die Seite
- Antworte laut, bevor du die Antwort öffnest. Beim Lesen hast du das Gefühl, es zu wissen; im Interview musst du es aussprechen.
- Beginne mit der kurzen Antwort und ergänze dann ein bis zwei Ideen aus „Dann geh tiefer“. Interviewer bevorzugen eine klare Position gegenüber einer langen Liste.
- Markiere, was du kannst, mit „Kann ich“. Das wird in diesem Browser gespeichert, und das Mock-Interview stellt dir zuerst die übrigen Fragen.
- Jede Antwort verlinkt auf die Lektion, die das Thema ausführlich erklärt.
Mock-Interview
Ein paar zufällige Fragen, eine nach der anderen, mit verborgener Antwort, bis du sie anforderst. Am Ende bekommst du die Liste der Fragen, die du dir noch einmal ansehen solltest.
Antworte laut, als würde dir jemand zuhören. Decke dann die Musterantwort auf und sei ehrlich zu dir selbst.
Junior: die Grundlagen
Für den ersten Angular-Job wird kein Wissen über Internals erwartet, wohl aber, dass du die modernen Grundlagen ohne Zögern beherrschst: Standalone-Komponenten, den neuen Control Flow, Signals, Inputs und Outputs, Services mit inject(), Routing und Formulare. Zeig, dass du das Warum kennst, nicht nur das Wie.
J-01Was ist eine Komponente und was bedeutet „Standalone“? Wie startet eine App ohne NgModule? Komponenten
Das sagst du zuerst
Eine Komponente ist eine TypeScript-Klasse mit
@Component, die ein Template mit Zustand und Verhalten verbindet und über ihren Selector verwendet wird. Standalone (der Standard) bedeutet, dass sie ihre Abhängigkeiten selbst inimportsdeklariert, ein NgModule ist also nicht nötig: Die App startet mitbootstrapApplication(App, appConfig).Dann geh tiefer
- Du schreibst
standalone: truenicht mehr — es ist der Standard. Jede Komponente importiert genau das, was ihr Template verwendet (andere Komponenten, Pipes,RouterLink), die Abhängigkeiten sind also explizit und Tree-Shaking-fähig. - Die anwendungsweite Konfiguration steht in
app.config.tsalsproviders:provideRouter(routes),provideHttpClient(...)nur, wenn du ihn konfigurierst, und so weiter. - Eine alte Bibliothek, die nur ein NgModule exportiert, kann trotzdem direkt in die
importseiner Komponente; Provider eines Moduls laufen überimportProvidersFrom().
import { bootstrapApplication } from '@angular/platform-browser'; import { App } from './app/app'; import { appConfig } from './app/app.config'; bootstrapApplication(App, appConfig).catch((err) => console.error(err));Das fragen sie wohl als Nächstes
Wo legst du einen Provider ab, den die gesamte Anwendung braucht?
Warnsignal
Sagt, dass jede Komponente in einem
AppModuledeklariert werden muss, oder importiertCommonModule„zur Sicherheit“.- Du schreibst
J-02Welche Arten von Data Binding gibt es in einem Template? Templates
Das sagst du zuerst
Interpolation
{{ value }}und Property Binding[prop]="expr"gehen von der Komponente zum DOM; Event Binding(event)="handler($event)"geht vom DOM zur Komponente; Two-Way-Binding[(value)]="sig"kombiniert beides.Dann geh tiefer
[disabled]bindet eine DOM-Property; für alles, was nur als Attribut existiert (ARIA,colspan), nutzt du[attr.aria-label].[(x)](„Banana in a Box“) ist nur syntaktischer Zucker für[x]="v()"+(xChange)="v.set($event)". Bei einem Signal übergibst du das Signal selbst, ohne Klammern.- Ausdrücke im Template sollten günstig und frei von Seiteneffekten sein; das Lesen eines Signals wie
count()ist in Ordnung, ein aufwendiger Methodenaufruf läuft bei jeder Prüfung.
<h2>{{ user().name }}</h2> <img [src]="user().avatarUrl" [attr.aria-label]="user().name" /> <button [disabled]="saving()" (click)="save()">Save</button> <input (input)="query.set($any($event.target).value)" /> <app-toggle [(checked)]="notifications" />Das fragen sie wohl als Nächstes
Was ist der Unterschied zwischen einem Attribut und einer Property?
Warnsignal
Hält
[value]="x"undvalue="x"für dasselbe (das zweite ist ein statischer String) oder schreibt[(checked)]="notifications()".J-03Wie schreibst du Bedingungen und Schleifen in einem Template und warum braucht
@foreintrack? TemplatesDas sagst du zuerst
Mit dem eingebauten Control Flow:
@if/@else,@formit@emptyund@switch/@case.trackist Pflicht: Es sagt Angular, wie jedes Element identifiziert wird, damit DOM-Knoten wiederverwendet werden, statt die ganze Liste neu zu erzeugen.Dann geh tiefer
- Tracke nach einer stabilen, eindeutigen ID (
track user.id);$indexnur für statische Listen; wer das Objekt selbst trackt, zerstört und erzeugt bei jedem Refetch das DOM neu. - Er ist Teil der Sprache: keine Imports von
NgIf/NgFor.@forstellt$index,$first,$last,$even,$odd,$countbereit. @if (user(); as u)liefert einen typisierten Alias ohne null. Seit v21.1 können sich mehrere@caseeinen Block teilen, und@default never;(v21.2) macht aus einem vergessenen Fall eines Unions einen Compile-Fehler.
@if (tasks().length) { <ul> @for (task of tasks(); track task.id; let i = $index) { <li>{{ i + 1 }}. {{ task.title }}</li> } </ul> } @else { <p>You have no tasks.</p> }Das fragen sie wohl als Nächstes
Was passiert mit dem DOM, wenn du nach
$indextrackst und die Liste dann sortierst?Warnsignal
Greift in neuem Code immer noch zu
*ngIf/*ngForoder kann nicht erklären, wofürtrackda ist.- Tracke nach einer stabilen, eindeutigen ID (
J-04Was ist
@letund wann würdest du es verwenden? TemplatesDas sagst du zuerst
@letdeklariert im Template eine lokale, schreibgeschützte Variable, die im aktuellen Block und seinen Kindern sichtbar ist. Ich nutze es, um einem langen, mehrfach verwendeten Ausdruck einen Namen zu geben oder einen Wert einmal auszupacken, z. B.@let user = user$ | async;.Dann geh tiefer
- Es kann im Template nicht neu zugewiesen werden; es wird neu berechnet, wenn sich ändert, was es liest.
- Praktisch in
@forfür einen Wert pro Element (z. B. ein Endpreis nach Rabatt). - Echte Logik gehört weiterhin in die Klasse, als
computed();@letdient der Lesbarkeit, nicht für Geschäftsregeln.
Warnsignal
Verwendet
@letwie eine veränderbare Variable oder verlagert damit komplexe Geschäftslogik ins Template.J-05Wie fügst du bedingt eine CSS-Klasse oder einen Style hinzu? Templates
Das sagst du zuerst
Mit nativen Bindings:
[class.active]="isActive()"für eine Klasse,[class]="{ ... }"für mehrere und[style.width.px]="width()"für Styles. Sie brauchen keinen Import, und statische Klassen werden mit den gebundenen kombiniert, nicht überschrieben.Dann geh tiefer
[class]akzeptiert einen String, ein Array oder ein Objekt;[style]akzeptiert einen String oder ein Objekt, und Einheiten stehen im Binding (.px,.%,.rem).[ngClass]/[ngStyle]sind der alte Weg; v21 bringt die Migrationenngclass-to-classundngstyle-to-stylemit.- Für das Host-Element der Komponente selbst nutzt du die
host-Metadaten ('[class.is-loading]': 'loading()'), nicht@HostBinding.
Das fragen sie wohl als Nächstes
Wie würdest du aus einem Signal eine CSS-Variable setzen?
Warnsignal
Baut Klassen-Strings von Hand in der Komponente zusammen oder manipuliert
classListdirekt am Element.J-06Was ist eine Pipe und was bedeutet „pure“? Templates
Das sagst du zuerst
Eine Pipe transformiert einen Wert für die Anzeige direkt im Template, etwa
{{ price | currency }}. Standardmäßig ist sie pure: Angular führt sie nur erneut aus, wenn sich der Eingabewert oder ihre Argumente ändern (per Referenz), sie ist also memoisiert.Dann geh tiefer
- Pipes sind Standalone und werden wie jede Abhängigkeit importiert (
imports: [DatePipe]); eine eigene Pipe ist eine Klasse mit@Pipeundtransform(). - Da per Referenz geprüft wird, läuft eine pure Pipe nicht erneut, wenn du ein Array an Ort und Stelle veränderst.
- Eine aus dem Template aufgerufene Methode läuft bei jeder Prüfung; für komponentenspezifische Berechnungen nimmst du besser
computed(), Pipes hebst du dir für wiederverwendbare Anzeigeformatierung auf. Vermeidepure: false.
Das fragen sie wohl als Nächstes
Würdest du zum Filtern einer Liste eine Pipe oder ein
computed()schreiben?Warnsignal
Macht eine Filter-Pipe impure, damit es auf einer großen Liste „läuft“.
- Pipes sind Standalone und werden wie jede Abhängigkeit importiert (
J-07Wie übergibt ein Parent Daten an eine Child-Komponente und wie antwortet das Child? Komponenten
Das sagst du zuerst
Das Child deklariert
input()für eingehende Daten undoutput()für ausgehende Events. Der Parent bindet[value]="..."und(rate)="onRate($event)"; das Child ruftrate.emit(4)auf.Dann geh tiefer
- Ein
input()ist ein schreibgeschütztes Signal: Du liest es mitvalue()und leitest mitcomputed()daraus ab —ngOnChangesbrauchst du nicht. input.required<T>()macht ein fehlendes Binding zum Compile-Fehler;transform: numberAttribute/booleanAttributewandelnmax="10"oder ein bloßesreadonly-Attribut in den richtigen Typ um.- Das Child kann auf seinem eigenen Input kein
set()aufrufen. Muss es den Wert ändern, sendet es ein Event und überlässt dem Parent die Entscheidung, oder es nutztmodel().
export class StarRating { readonly value = input.required<number>(); readonly max = input(5, { transform: numberAttribute }); readonly rate = output<number>(); protected readonly stars = computed(() => Array.from({ length: this.max() }, (_, i) => i + 1), ); }Das fragen sie wohl als Nächstes
Was ändert sich, wenn das Child den empfangenen Wert ändern muss?
Warnsignal
Sagt, man brauche
ngOnChanges, um auf einen Input zu reagieren, oder verwendet@Input/EventEmitterin neuem Code.- Ein
J-08Was ist
model()und wie unterscheidet es sich voninput()+output()? KomponentenDas sagst du zuerst
model()ist ein beschreibbares Signal, das als Input plus automatischerxChange-Output funktioniert, sodass der Parent Two-Way-Binding nutzen kann:[(checked)]="notifications". Ruft das Childset()oderupdate()auf, aktualisiert sich das Signal des Parents.Dann geh tiefer
- Nutze
model(), wenn der Wert wirklich beiden Seiten gehört, etwa bei einem Toggle, einem Stepper oder einem eigenen Input. - Nutze
input()+output(), wenn das Child nur vorschlägt und der Parent entscheidet (validiert, bestätigt, auf dem Server speichert). - Beim Two-Way-Binding übergibst du das Signal selbst (
notifications), nicht seinen Wert (notifications()).
Warnsignal
Kann nicht erklären, wozu sich
[( )]aufklappt, oder nutztmodel()für alles.- Nutze
J-09Was ist Content Projection? Komponenten
Das sagst du zuerst
Damit kann ein Parent sein eigenes Markup zwischen die Tags einer Komponente setzen, und die Komponente entscheidet mit
<ng-content />, wo es gerendert wird. Mitselectsind mehrere Slots möglich, etwa ein Titel und Aktionen in einer Card.Dann geh tiefer
selectnimmt einen Tag-, Attribut- oder Klassen-Selector ([card-title]); das<ng-content />ohneselectfängt alles Übrige auf, und du kannst darin Fallback-Inhalt platzieren.- Stolperfalle: Projizierter Inhalt wird vom Parent erzeugt, selbst wenn das Child ihn mit
@ifversteckt. Für wirklich lazy Inhalt übergibst du ein<ng-template>. - Projection ist für flexibles Markup (Layouts, Cards, Dialoge); Inputs sind für Daten, die die Komponente selbst formatiert.
Das fragen sie wohl als Nächstes
Wann würdest du Projection statt eines Inputs wählen?
Warnsignal
Übergibt HTML als String-Input und rendert es mit
innerHTML, statt es zu projizieren.J-10Was sind
signal()undcomputed()? SignalsDas sagst du zuerst
Ein
signal()ist ein reaktiver Wert: Du liest ihn, indem du ihn aufrufst, und änderst ihn mitset()oderupdate(). Eincomputed()ist ein schreibgeschützter, aus anderen Signals abgeleiteter Wert; er ist lazy und memoisiert und wird nur neu berechnet, wenn sich seine Abhängigkeiten ändern.Dann geh tiefer
- Abhängigkeiten werden automatisch verfolgt: Alle Signals, die die Funktion liest, werden zu Abhängigkeiten, ohne dass du Subscriptions verwalten musst.
- Signals vergleichen mit
===:items().push(x)benachrichtigt niemanden; nutzeupdate((list) => [...list, x]). - Faustregel: Alles, was sich aus anderem Zustand berechnen lässt, ist ein
computed(), kein zweites, synchron gehaltenes Signal. Gib Zustand mitasReadonly()nach außen.
export class Counter { protected readonly count = signal(0); protected readonly double = computed(() => this.count() * 2); protected increment() { this.count.update((c) => c + 1); } } // template: {{ count() }} · {{ double() }}Das fragen sie wohl als Nächstes
Warum aktualisiert sich die UI nach
items().push(x)nicht?Warnsignal
Versucht
set()auf einemcomputed()oder vergisst die Klammern und bindet die Signal-Funktion statt ihres Werts.J-11Was ist
effect()und wann solltest du es nicht verwenden? SignalsDas sagst du zuerst
effect()führt eine Funktion einmal aus und dann erneut, sobald sich die gelesenen Signals ändern. Es ist für Seiteneffekte nach außen gedacht —localStorage, Logging, eine Drittanbieter-Bibliothek — nicht zum Berechnen von Zustand.Dann geh tiefer
- Nutze kein Effect, um ein Signal in ein anderes zu kopieren: Das ist ein
computed()(oderlinkedSignal(), wenn es auch editierbar sein muss). - Lade keine Daten aus einem Effect: Es gibt keine Abbruchmöglichkeit, und alte Responses können das Rennen gewinnen. Nutze
httpResource()/resource(). - Es muss in einem Injection-Kontext erstellt werden (Konstruktor oder Feld), wird mit der Komponente zerstört, und
onCleanupräumt Timer vor dem nächsten Lauf auf.
Das fragen sie wohl als Nächstes
Was passiert, wenn du ein Effect in
ngOnIniterstellst?Warnsignal
Nutzt
effect(), umfullNamemitfirstNameundlastNamesynchron zu halten.- Nutze kein Effect, um ein Signal in ein anderes zu kopieren: Das ist ein
J-12Was ist der Unterschied zwischen Konstruktor und
ngOnInit? Wie räumst du auf, wenn eine Komponente zerstört wird? KomponentenDas sagst du zuerst
Der Konstruktor läuft beim Erzeugen der Klasse und ist der Injection-Kontext:
inject(), Signals und Effects gehören dorthin oder in Feld-Initialisierer.ngOnInitläuft einmal, nachdem die ersten Inputs gesetzt sind. Zum Aufräumen nutze ichinject(DestroyRef).onDestroy()odertakeUntilDestroyed().Dann geh tiefer
- Das Lesen eines
input.required()im Konstruktor wirft einen Fehler, weil der Parent es noch nicht gebunden hat. Mit Signal-Inputs brauchst dungOnInitselten: Eincomputed()oderhttpResource()liest den Input, wenn er bereit ist. inject()funktioniert nicht inngOnInitoder in einem Click-Handler — das liegt außerhalb des Injection-Kontexts (Fehler NG0203).DestroyReffunktioniert auch in Services, Direktiven und wiederverwendbaren Funktionen, wasngOnDestroynicht kann. Für DOM-Zugriff nach dem Rendern nutzeafterNextRender().
export class Clock { readonly now = signal(new Date()); constructor() { const id = setInterval(() => this.now.set(new Date()), 1000); inject(DestroyRef).onDestroy(() => clearInterval(id)); } }Das fragen sie wohl als Nächstes
Wo würdest du eine Chart-Bibliothek initialisieren, die das DOM-Element braucht?
Warnsignal
Sagt, der Konstruktor sei „nur für DI“, und packt die gesamte Logik in
ngOnInit, oder räumt Intervalle und Subscriptions nie auf.- Das Lesen eines
J-13Was ist ein Service und wie nutzt du ihn in einer Komponente? DI
Das sagst du zuerst
Ein Service ist eine Klasse, die Logik oder Zustand hält, der von mehreren Komponenten geteilt wird — API-Aufrufe, ein Warenkorb, der angemeldete Benutzer. In v22 markiere ich ihn mit
@Service(), der als Singleton im Root bereitgestellt wird, und hole ihn mitinject(CartService).Dann geh tiefer
@Service()entspricht@Injectable({ providedIn: 'root' }): wird bei der ersten Injection erzeugt und ist Tree-Shaking-fähig. Es erfordertinject()— Konstruktor-DI ist ein Compile-Fehler.@Injectablebleibt gültig (bestehender Code, Bibliotheken für ältere Versionen). Legst du den Service in dieproviderseiner Komponente, bekommst du eine Instanz pro Komponente statt einer pro App.inject()funktioniert auch in funktionalen Guards, Resolvern und Interceptors und vermeidet langesuper(...)-Aufrufe bei Vererbung.
@Service() export class CartService { private readonly _items = signal<CartItem[]>([]); readonly items = this._items.asReadonly(); readonly count = computed(() => this._items().length); add(item: CartItem) { this._items.update((list) => [...list, item]); } }Das fragen sie wohl als Nächstes
Wie würdest du jeder Instanz einer Wizard-Komponente ihre eigene Kopie eines Services geben?
Warnsignal
Erzeugt den Service mit
new CartService()oder glaubt, ein Service inproviderssei weiterhin derselbe globale Singleton.J-14Wie richtest du Routing ein und navigierst zwischen Seiten? Routing
Das sagst du zuerst
Du definierst ein
Routes-Array, registrierst es mitprovideRouter(routes)und setzt<router-outlet />dorthin, wo die Seiten gerendert werden. Navigiert wird mitrouterLinkin Templates undrouter.navigate()im Code; Seiten werden meist perloadComponentlazy geladen.Dann geh tiefer
loadComponent: () => import('./home-page')legt die Seite in einen eigenen Chunk, der nur beim Besuch der Route geladen wird;loadChildrenmacht dasselbe für ein ganzes Feature.- Die Reihenfolge zählt — der erste Treffer gewinnt —, daher steht der Wildcard
'**'ganz am Ende.routerLinkActivefügt dem aktiven Link eine Klasse hinzu. - Nutze
routerLink, nichthref:hreflädt die gesamte Anwendung neu.
Das fragen sie wohl als Nächstes
Wie würdest du eine Route schützen, sodass nur angemeldete Benutzer sie öffnen können?
Warnsignal
Navigiert mit
<a href>oderwindow.locationoder setzt die Wildcard-Route an die erste Stelle.J-15Wie liest eine Seite einen Route-Parameter wie
:id? RoutingDas sagst du zuerst
Mit
withComponentInputBinding()inprovideRouterkommen Pfad-Parameter, Query-Parameter und Route-Daten als Inputs der Komponente an:readonly id = input.required({ transform: numberAttribute }). Da es ein Signal ist, aktualisiert sich alles, was daraus abgeleitet ist, wenn sich die ID ändert.Dann geh tiefer
- Stolperfalle: Die Navigation von
/products/1zu/products/2verwendet dieselbe Komponenteninstanz weiter; Code, der die ID nur einmal liest (z. B. ein Snapshot inngOnInit), zeigt veraltete Daten. - Parameter sind in der URL Strings, daher liefert dir ein Transform wie
numberAttributeeine echte Zahl. - Seit v22 erben Child-Routen standardmäßig Params und Data aller Parents (
paramsInheritanceStrategy: 'always').
@Component({ templateUrl: './product-page.html' }) export default class ProductPage { readonly id = input.required({ transform: numberAttribute }); // :id readonly tab = input<'specs' | 'reviews'>('specs'); // ?tab= readonly product = httpResource<Product>(() => '/api/products/' + this.id()); }Das fragen sie wohl als Nächstes
Was geht kaputt, wenn du die ID nur einmal mit
route.snapshotliest?Warnsignal
Kennt nur
route.snapshot.paramMap.get('id')und weiß nicht, dass die Komponente wiederverwendet werden kann.- Stolperfalle: Die Navigation von
J-16Wie rufst du in Angular eine REST-API auf? HTTP
Das sagst du zuerst
Mit
HttpClient, in einen Service injiziert:http.get<Product[]>('/api/products')gibt ein Observable zurück. Seit v21 ist er ohne Setup im Root verfügbar;provideHttpClient()ist nur für Konfiguration wie Interceptors nötig.Dann geh tiefer
- Das Observable ist cold: Es wird kein Request gesendet, bis sich jemand subscribt (
subscribe(),firstValueFrom(), die Async-Pipe). - Für reaktive Lesezugriffe in einer Komponente liefert
httpResource()value(),isLoading()underror()als Signals und bricht alte Requests ab; für Schreibzugriffe (POST, PUT) rufst duHttpClientauf. - Das Generic
get<T>ist nur eine Typ-Assertion, keine Validierung. In v22 ist Fetch das Standard-Backend; übergreifende Belange (Auth-Token, Fehler) gehören in funktionale Interceptors.
Das fragen sie wohl als Nächstes
Wo würdest du bei jedem Request einen
Authorization-Header hinzufügen?Warnsignal
Ruft
http.get()ohne Subscribe auf und wundert sich, dass nichts passiert, oder ruftfetchdirekt aus Komponenten auf.- Das Observable ist cold: Es wird kein Request gesendet, bis sich jemand subscribt (
J-17Wie baust du in Angular 22 ein Formular mit Validierung? Formulare
Das sagst du zuerst
Für neue Formulare nutze ich Signal Forms, stabil in v22: Ein Signal hält das Modell,
form(model, schema)baut einen Baum aus Feldern mit Regeln wierequired(p.email), und[formField]bindet jedes Input. Reactive Forms bleiben die Wahl für bestehende Apps, die voll davon sind, oder für Bibliotheken, die nurControlValueAccessorunterstützen.Dann geh tiefer
- Das Modell-Signal ist die einzige Quelle der Wahrheit — keine interne Kopie, kein
patchValue:model.set(data)füllt das Formular, und das Formular schreibt direkt ins Modell. - Der Zustand jedes Felds besteht aus Signals:
f.email().invalid(),touched(),errors(); auch das gesamte Formular ist ein Feld (f().valid(),f().submitting()). - Zeige Fehler erst nach
touched()und validiere immer auch auf dem Server — Client-Validierung ist UX, keine Sicherheit.
export class LoginForm { readonly model = signal({ email: '', password: '' }); readonly loginForm = form(this.model, (p) => { required(p.email); email(p.email); minLength(p.password, 8); }); } // template: <input [formField]="loginForm.email" />Das fragen sie wohl als Nächstes
Wie würdest du validieren, dass zwei Passwortfelder übereinstimmen?
Warnsignal
Validiert von Hand im Submit-Handler mit
ifs oder sagt, Signal Forms seien in v22 noch nur experimentell.- Das Modell-Signal ist die einzige Quelle der Wahrheit — keine interne Kopie, kein
J-18Was macht
@defer? PerformanceDas sagst du zuerst
@deferlädt einen Teil eines Templates lazy: Die darin enthaltenen Komponenten landen in einem eigenen Chunk, der erst geladen wird, wenn ein Trigger auslöst — standardmäßigon idle, oderon viewport,on interaction,when cond(). Es verkleinert das initiale Bundle, ohne die Routen anzufassen.Dann geh tiefer
@placeholderwird vor dem Trigger angezeigt (nötig füron viewport/interaction/hover),@loadingwährend des Downloads,@error, falls der Download fehlschlägt.- Stolperfalle: Wird eine Deferred-Komponente in derselben Datei auch außerhalb des Blocks referenziert, wird sie eager geladen und der Chunk geht verloren.
prefetch on hoverlädt früh herunter und rendert später — gut für Aktionen, die „einen Klick entfernt“ sind.
@defer (on viewport) { <app-comments [postId]="post().id" /> } @placeholder { <div class="comments-skeleton"></div> } @loading (after 100ms) { <app-spinner /> }Das fragen sie wohl als Nächstes
Was ist der Unterschied zwischen
@deferund@if?Warnsignal
Hält
@deferfür ein bloß verzögertes@ifund erwähnt den separaten JavaScript-Chunk nicht.J-19Woher weiß Angular, wann der Bildschirm aktualisiert werden muss? Was hat sich in v21 und v22 geändert? Change Detection
Das sagst du zuerst
Change Detection rendert Templates neu, wenn etwas Angular benachrichtigt: Ein im Template gelesenes Signal ändert sich, ein Template-Event feuert, die Async-Pipe emittiert oder
markForCheck()wird aufgerufen. Seit v21 sind neue Apps zoneless (ohne Zone.js), und seit v22 sind Komponenten standardmäßigOnPush.Dann geh tiefer
- Ohne Zone.js aktualisiert ein
setTimeoutodersubscribe(), das ein einfaches Feld ändert, die UI nicht. Die Lösung ist ein Signal (odertoSignal()/ die Async-Pipe). - Eine
OnPush-Komponente wird nur geprüft, wenn ein Input eine neue Referenz bekommt, in ihr ein Event auftritt, sich ein von ihr gelesenes Signal ändert oder sie zur Prüfung markiert wird — sonst wird ihr Teilbaum übersprungen. ChangeDetectionStrategy.Eagerist das alte Verhalten „immer prüfen“ (Defaultist deprecated);ng updatefügt es dort ein, wo es nötig ist.
Das fragen sie wohl als Nächstes
Ein in
setTimeoutgesetzter Wert erscheint nicht auf dem Bildschirm. Warum, und wie behebst du das?Warnsignal
Behauptet bei einer v21+-App, Zone.js erkenne jede Änderung automatisch, oder behebt fehlende Updates, indem überall
detectChanges()verstreut wird.- Ohne Zone.js aktualisiert ein
J-20Wie testest du eine Komponente in einem neuen Angular-Projekt? Testing
Das sagst du zuerst
Seit v21 führt
ng testVitest aus (Karma ist deprecated). Ich erzeuge die Komponente mitTestBed.createComponent(), setze Inputs, rufeawait fixture.whenStable()auf, damit das Zoneless-Rendering abgeschlossen ist, und prüfe dannfixture.nativeElement.Dann geh tiefer
- Teste das Verhalten, das der Benutzer sieht (Text, Klicks, emittierte Outputs), nicht private Implementierungsdetails.
- Inputs lassen sich mit
inputBinding()als Signals übergeben; Outputs werden mitoutputBinding()undvi.fn()überwacht. - Für HTTP nutzt du
provideHttpClientTesting()undHttpTestingControllerstatt eines echten Servers; Services und Signal-Stores lassen sich mitTestBed.inject()ohne DOM testen.
it('shows the right number of stars', async () => { const fixture = TestBed.createComponent(StarRating, { bindings: [inputBinding('value', () => 3)], }); await fixture.whenStable(); const stars = fixture.nativeElement.querySelectorAll('button'); expect(stars).toHaveLength(5); });Das fragen sie wohl als Nächstes
Wie würdest du eine Komponente testen, die Daten per HTTP lädt?
Warnsignal
Hat nie einen Test geschrieben oder verlässt sich in einem Zoneless-Vitest-Projekt auf
fakeAsync/tickvon Zone.js.
Mid: Features eigenständig bauen
Auf diesem Level geht es in den Fragen um Entscheidungen: computed oder effect, welcher RxJS-Operator, wo ein Provider liegt, wie du eine Komponente aufteilst, wie du sie testest. Eine gute Mid-Antwort nennt die Variante, den Grund und den Fall, in dem die andere Variante gewinnt.
M-01Wann nutzt du
computed()und wanneffect()? Warum solltest du in einem Effect keine Signals setzen? SignalsDas sagst du zuerst
computed()ist für abgeleiteten Zustand: ein reiner, lazy ausgewerteter, gememoizter Wert.effect()ist für Seiteneffekte, die Signal-Werte in die nicht-reaktive Welt schreiben (DOM,localStorage, Logging). Signal → Signal über einen Effect ist fast immer ein verkapptescomputed()oderlinkedSignal().Dann geh tiefer
- Ein Signal in einem Effect zu setzen bedeutet einen zusätzlichen Durchlauf, einen Moment mit veraltetem Ziel (ein Render mit falschem Wert) und einen schwer nachvollziehbaren Abhängigkeitsgraphen.
- Effects laufen nicht synchron: Setzt du ein Signal dreimal, läuft der Effect einmal, mit dem finalen Wert. Komponenten-Effects laufen während der Change Detection; Effects in Root-Services laufen als Microtasks.
- Abhängigkeiten sind dynamisch: Nur Signals, die im letzten Durchlauf gelesen wurden, werden verfolgt. Nutze
untracked()für Lesezugriffe, die den Effect nicht erneut auslösen sollen. - Ein Effect braucht einen Injection-Kontext (Konstruktor oder Feld-Initialisierung) oder einen expliziten
injector; sonst bekommst du NG0203. Für Timer und Listener nutzt duonCleanup. - HTTP aus einem Effect ist ein Code Smell: keine Abbrechbarkeit, Antworten können also in falscher Reihenfolge ankommen. Dafür gibt es
resource/httpResource.
readonly firstName = signal('Ana'); readonly lastName = signal('Pop'); // ✗ effect used to synchronise state // effect(() => this.fullName.set(this.firstName() + ' ' + this.lastName())); // ✓ derived state readonly fullName = computed(() => this.firstName() + ' ' + this.lastName()); // ✓ effect: reactive world → outside world constructor() { effect(() => localStorage.setItem('name', this.fullName())); }Das fragen sie wohl als Nächstes
Was, wenn der abgeleitete Wert auch vom Benutzer editierbar sein soll?
Warnsignal
„Ich nutze Effect, um zwei Signals synchron zu halten“ oder „computed und effect sind im Grunde dasselbe, effect läuft nur öfter“.
M-02Welches Problem löst
linkedSignal()? Nenne ein reales Beispiel. SignalsDas sagst du zuerst
Es ist ein beschreibbares Signal, das sich aus einer Quelle zurücksetzt: abgeleitet wie
computed(), aber du kannst es auch mitset()setzen. Typische Fälle: eine Auswahl, die standardmäßig die erste Option einer Liste ist, eine Seitenzahl, die bei geänderten Filtern auf 1 zurückspringt, eine editierbare Kopie eines Inputs.Dann geh tiefer
- Es ersetzt das fragile Muster aus Signal + Effect: kein zusätzlicher Durchlauf, kein Render mit veraltetem Wert.
- Die lange Form (
source+computation) liefert dir denprevious-Wert, sodass du die Auswahl des Benutzers behalten kannst, wenn sie in der neuen Liste noch existiert. - Ein manuelles
set()gilt nur, bis sich die Quelle wieder ändert; danach gewinnt die Computation. - Seit v22.1 fängt die Option
setSchreibzugriffe ab, zum Beispiel um eine Menge auf den verfügbaren Bestand zu begrenzen.
Das fragen sie wohl als Nächstes
Wie würdest du einen editierbaren Entwurf eines
input()mit einem „dirty“-Flag bauen?Warnsignal
Den Input in
ngOnInitin ein Signal zu kopieren (es wird nie zurückgesetzt) oder per Effect zu synchronisieren.M-03Wie funktionieren
resource/httpResource, und warum sind sie besser, als HTTP in einem Effect zu abonnieren? HTTPDas sagst du zuerst
Eine Resource macht asynchrones Laden zu Signals: eine reaktive
params-Funktion (oder URL); ändert sie sich, wird der alte Request abgebrochen und ein neuer startet.value(),status(),isLoading(),error()undreload()bekommst du geschenkt, ohne Race Conditions.Dann geh tiefer
resourcebekommt im Loader einabortSignal;httpResourceläuft überHttpClient, daher greifen Interceptors;rxResourcenimmt einen Observable-Stream und verhält sich wieswitchMap.- Gibt params
undefinedzurück, ist der Statusidle: Der Loader läuft nicht. In v22 kannst du auchResourceParamsStatus.IDLE/LOADINGwerfen und Resources mitctx.chain()verketten. hasValue()ist ein Type Guard;defaultValueentferntundefined;parsevalidiert oder mappt die Antwort (z. B. DTO → Modell, Zod).- Resources sind zum Lesen da. Schreibzugriffe (POST/PUT/DELETE) laufen über
HttpClient; danach aktualisierst du die Resource lokal mitupdate()(optimistisch) oder lädst sie mitreload()neu.
readonly userId = input.required<number>(); // a new id → the old request is cancelled, a new one starts readonly user = httpResource<User>(() => '/api/users/' + this.userId()); // template: // @if (user.isLoading()) { <app-spinner /> } // @if (user.error()) { <button (click)="user.reload()">Retry</button> } // @else if (user.hasValue()) { <h2>{{ user.value().name }}</h2> }Das fragen sie wohl als Nächstes
Wie würdest du ein Element aus einer mit
httpResourcegeladenen Liste löschen und einen Fehlschlag behandeln?Warnsignal
httpResourcefür einen POST beim Button-Klick zu verwenden oder nicht zu wissen, was mit einem laufenden Request passiert, wenn sich params ändern.M-04Wie implementierst du eine Suche während der Eingabe, ohne bei jedem Tastendruck einen Request abzufeuern? Signals
Das sagst du zuerst
In v22 wickle ich das Query-Signal mit
debounced(query, 300)ein und gebe dessenvalue()an einhttpResource. Das Debounce filtert das Rauschen; die Resource bricht veraltete Requests ab, sodass immer die letzte Suche gewinnt.Dann geh tiefer
debounced()gibt eineResourcezurück:value()aktualisiert sich nach der letzten Änderung, undisLoading()ist true, solange der Benutzer noch tippt.- Es ist in v22 experimentell. Die klassische Alternative ist
toSignal(toObservable(query).pipe(debounceTime(300), distinctUntilChanged())): drei Umwandlungen für ein einziges Debounce. waitkann eine Funktion sein, z. B. ohne Verzögerung, wenn das Feld geleert wird.- In Signal Forms gibt es eine eigene Regel
debounce(path.field, 300), undvalidateHttphat eine eigenedebounce-Option.
import { httpResource } from '@angular/common/http'; import { debounced, signal } from '@angular/core'; export class UserSearch { readonly query = signal(''); readonly search = debounced(this.query, 300); readonly users = httpResource<UserDto[]>( () => '/api/users?q=' + encodeURIComponent(this.search.value()), { defaultValue: [] }, ); }Das fragen sie wohl als Nächstes
Debounce allein verhindert keine Antworten in falscher Reihenfolge. Was dann?
Warnsignal
Ein
setTimeoutim Input-Handler mit manuellemsubscribe()und ohne Abbruch des vorherigen Requests.M-05Wie kombinierst du Signals und RxJS in einer Komponente, ohne Subscriptions offen zu lassen? Wann behältst du RxJS? RxJS
Das sagst du zuerst
Signals für den angezeigten Zustand, RxJS an den Rändern (Events, Streams, komplexes Async).
toSignal()abonniert und beendet das Abo automatisch; bei einem manuellensubscribe()setze ichtakeUntilDestroyed()ein, das ihn an denDestroyRefder Komponente bindet.Dann geh tiefer
toSignal()ohneinitialValuehat den TypT | undefined;requireSync: truefunktioniert nur, wenn die Quelle synchron emittiert (z. B. einBehaviorSubject).toObservable()emittiert asynchron (über einen Effect) und nur den letzten Wert, wenn das Signal im selben Tick mehrfach gesetzt wird — es ist kein Event-Stream.takeUntilDestroyed()ohne Argument funktioniert nur in einem Injection-Kontext; sonst übergibst du explizitinject(DestroyRef). Für Cleanup ohne RxJS nutzt duDestroyRef.onDestroy().- RxJS behältst du für Websockets, das Kombinieren von Event-Streams, Retry/Backoff und Flattening-Strategien (
switchMap,exhaustMap…). Signals kennen keine Zeit.
Das fragen sie wohl als Nächstes
Warum kannst du
toSignal()nicht in einem Click-Handler aufrufen?Warnsignal
„Signals ersetzen RxJS komplett“ — oder das Gegenteil: überall subscriben und Werte in Felder kopieren, ohne je abzumelden.
M-06Erkläre
switchMapvs.mergeMapvs.concatMapvs.exhaustMap, mit je einem Anwendungsfall. RxJSDas sagst du zuerst
Sie unterscheiden sich darin, was passiert, wenn ein neuer Wert eintrifft, während das vorherige innere Observable noch läuft:
switchMapbricht es ab (Suche),mergeMapführt parallel aus (Uploads),concatMapreiht der Reihe nach ein (geordnetes Speichern),exhaustMapignoriert den neuen Wert (Submit, Login).Dann geh tiefer
- Die Wahl des Operators ist die Wahl einer Concurrency-Strategie; die falsche verursacht echte Bugs: doppelte Bestellungen mit
mergeMapauf dem Bezahlen-Button, verlorene Speichervorgänge mitswitchMapbeim Autosave. mergeMapakzeptiert ein Concurrency-Limit, z. B.mergeMap(upload, 3).- Setze
catchErrorin die innere Pipe: Nach dem Flattening-Operator beendet der erste Fehler den äußeren Stream, und die Suche „stirbt“. - Mit Signals liefern dir
httpResourceundrxResourcefür Lesezugriffe bereits die Semantik vonswitchMap.
// the last one wins; errors handled INSIDE, so the stream survives query$.pipe( debounceTime(300), switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))), ); files$.pipe(mergeMap((file) => this.api.upload(file), 3)); // parallel, max 3 patches$.pipe(concatMap((patch) => this.api.save(patch))); // in order submit$.pipe(exhaustMap(() => this.api.login(this.credentials()))); // ignore double clicksDas fragen sie wohl als Nächstes
Welchen würdest du für das Autosave eines Dokuments nehmen, und warum nicht
switchMap?Warnsignal
„Ich nutze immer
switchMap“ oder verschachteltesubscribe()-Aufrufe statt eines Flattening-Operators.- Die Wahl des Operators ist die Wahl einer Concurrency-Strategie; die falsche verursacht echte Bugs: doppelte Bestellungen mit
M-07Was löst in einer zoneless Angular-22-App die Change Detection aus, und warum erscheint ein in
setTimeoutaktualisiertes Feld womöglich nicht? Change DetectionDas sagst du zuerst
Ohne Zone.js patcht niemand Timer oder Promises. Change Detection wird geplant, wenn sich ein im Template gelesenes Signal ändert, bei einem Template-/Host-Event, durch die
async-Pipe,markForCheck(),setInput()oder eine Navigation. Ein einfaches Feld, das insetTimeoutodersubscribe()gesetzt wird, benachrichtigt niemanden — mach es zu einem Signal.Dann geh tiefer
- Seit v22 sind Komponenten ohne
changeDetectionOnPush; das alte Verhalten istChangeDetectionStrategy.Eager(Defaultist deprecated).ng updateergänzt Eager, wo nötig. - Eine OnPush-Komponente wird nur geprüft bei einer neuen Input-Referenz (
===), einem Event in ihr oder ihren Kindern, einem im Template gelesenen Signal, derasync-Pipe odermarkForCheck(). Sonst wird der gesamte Teilbaum übersprungen. - Ein als Input übergebenes Array zu mutieren (
push) ändert die Referenz nicht: Nutze unveränderliche Updates. markForCheck()ist die Notlösung für Legacy-Callbacks;detectChanges()ist synchron und selten nötig.
// ✗ works with zone.js, NOT zoneless + OnPush // now = new Date(); // setInterval(() => (this.now = new Date()), 1000); // ✓ a signal schedules change detection by itself export class Clock { readonly now = signal(new Date()); constructor() { const id = setInterval(() => this.now.set(new Date()), 1000); inject(DestroyRef).onDestroy(() => clearInterval(id)); } }Das fragen sie wohl als Nächstes
Wie würdest du eine ältere Komponente, die auf Zone.js setzt, Schritt für Schritt auf OnPush + zoneless migrieren?
Warnsignal
Überall
detectChanges()aufzurufen oder zu glauben, OnPush heiße „die Komponente aktualisiert sich nur, wenn sich Inputs ändern“.- Seit v22 sind Komponenten ohne
M-08Wie unterscheiden sich
viewChild()undcontentChildren()von den alten Dekoratoren, und wann kannst du sie sicher lesen? KomponentenDas sagst du zuerst
Signal-Queries liefern Signals, die Ergebnisse sind also reaktiv: Du leitest daraus mit
computed()ab statt mitngAfterViewInitundQueryList.changes. Im Konstruktor existiert die View noch nicht, DOM-Arbeit gehört also inafterNextRender();viewChild.required()wirft einen Fehler, wenn du zu früh liest.Dann geh tiefer
viewChildsucht im eigenen Template der Komponente;contentChildsucht in dem, was der Parent projiziert hat (z. B. ein Tabs-Container, der seine Tab-Kinder findet).contentChildrenbetrachtet standardmäßig nur direkte Kinder; für tiefere Treffer nutzt dudescendants: true.readbestimmt, was du bekommst:ElementRef,TemplateRef,ViewContainerRef, eine Direktive oder (v22.2) denInjectordes Kindes.- Stolperfalle: Wird eine Komponente in der Klasse per Typ abgefragt, wird sie eager, und ein
@defer-Block lagert sie nicht mehr in einen eigenen Chunk aus.
Das fragen sie wohl als Nächstes
Wie würde eine Tabs-Komponente nur den Inhalt des aktiven Tabs rendern?
Warnsignal
Eine View-Query im Konstruktor oder in
ngOnInitzu lesen und sich zu wundern, dass sieundefinedist.M-09Content Projection mit
ng-contentvs. ein mitngTemplateOutletgerendertesng-template: Wann wählst du was? TemplatesDas sagst du zuerst
ng-contentist für statische Slots: Der Parent erzeugt den Inhalt einmal, egal ob das Kind ihn anzeigt. Einng-templateist ein Rezept, das das Kind wann und so oft es will instanziiert, mit einem Kontext — nötig für Lazy-Inhalte und für APIs im Stil von „wie soll eine Zeile aussehen“.Dann geh tiefer
- Wenn du
ng-contentin ein@ifpackst, blendest du das DOM aus, aber die projizierten Komponenten existieren bereits und ihr Lifecycle ist schon gelaufen. - Benannte Slots nutzen
select(Tag, Attribut, Klasse);ngProjectAsprojiziert mehrere Elemente ohne Wrapper in einen Slot. - Der Kontext reicht Daten zurück:
$implicitlandet inlet-item. Ohne eine Direktive mitngTemplateContextGuardsind dielet--Variablenany. - Ein Template sieht den Injector der Stelle, an der es deklariert wurde; seit v21.2 sorgt
[ngTemplateOutletInjector]="'outlet'"dafür, dass es den Injector der Stelle nutzt, an der es gerendert wird.
@Component({ selector: 'app-collapsible', imports: [NgTemplateOutlet], template: ` <button (click)="open.set(!open())">Toggle</button> @if (open()) { <ng-container [ngTemplateOutlet]="content() ?? null" /> } `, }) export class Collapsible { readonly content = contentChild(TemplateRef); // created only when opened readonly open = signal(false); } // <app-collapsible><ng-template><app-heavy-report /></ng-template></app-collapsible>Das fragen sie wohl als Nächstes
Wie machst du die
let--Variablen eines Templates streng typisiert?Warnsignal
Zu glauben, ein
@ifumng-contentmache den projizierten Inhalt lazy.- Wenn du
M-10Wie würdest du eine wiederverwendbare, typsichere Datentabellen-Komponente bauen, ohne dass sie zur „God Component“ wird? Komponenten
Das sagst du zuerst
Mach die Komponente generisch (
DataTable<T>):Twird ausrowsinferiert, Spaltenschlüssel sind aufkeyof Tbeschränkt, Outputs sind typisiert. Anpassungen laufen über Zellen-Templates statt über boolesche Inputs, und domänenspezifische Wrapper leben im Feature.Dann geh tiefer
- Der God-Component-Geruch:
any[]-Daten,isProductTable-Flags, 30 Inputs und Domänenwissen inshared/. - Zellen-Templates findet man mit
contentChildren()an einer Direktive;ngTemplateContextGuardtypisiertlet-rowalsTstatt alsany. - Übergib Verhalten als Funktionen (
trackBy,displayWith,compareWith), damit die Komponente die Form der Daten nicht kennen muss. - Ein Tippfehler im Spaltenschlüssel wird zum Compile-Fehler statt zu einer leeren Spalte in Produktion.
Das fragen sie wohl als Nächstes
Wo würde die Benutzer-spezifische Tabelle liegen, und was würde sie zur generischen hinzufügen?
Warnsignal
Jedes Mal einen weiteren booleschen Input hinzuzufügen, wenn eine Seite etwas anderes braucht.
- Der God-Component-Geruch:
M-11Wie renderst du eine zur Laufzeit gewählte Komponente, z. B. Dashboard-Widgets aus einer Konfiguration? Komponenten
Das sagst du zuerst
Steig eine Leiter hinauf: kleine, bekannte Menge →
@switch; die Komponente ist ein Wert →NgComponentOutletmitinputs; volle Kontrolle (Outputs, Direktiven) →ViewContainerRef.createComponent()mitbindings(inputBinding,outputBinding,twoWayBinding).Dann geh tiefer
- Eine Registry aus
() => import(…)-Loadern macht jedes Widget zu einem eigenen Lazy-Chunk. inputBinding('title', () => …)ist reaktiv, anders als das altesetInput(), das du bei jeder Änderung erneut aufrufen musstest, plus manuelle Output-Subscriptions.ComponentFactoryResolverwurde in v22 entfernt: Du übergibst die Klasse direkt ancreateComponent.- Für Toasts und Dialoge außerhalb des Baums nutzt du die Standalone-Funktion
createComponent()mit einemEnvironmentInjector, danachappRef.attachView(), damit sie an der Change Detection teilnimmt, und zerstörst sie selbst.
private readonly vcr = inject(ViewContainerRef); private async render(type: WidgetType) { const component = await WIDGET_REGISTRY[type](); // lazy chunk this.vcr.clear(); this.vcr.createComponent(component, { bindings: [ inputBinding('title', () => this.config().title), // stays in sync outputBinding('remove', () => this.removed.emit(this.config().id)), ], }); }Das fragen sie wohl als Nächstes
Wie stellst du sicher, dass alle Widgets dieselben Inputs und Outputs akzeptieren?
Warnsignal
ComponentFactoryResolveroderinnerHTMLmit Komponenten-Tags zu verwenden.- Eine Registry aus
M-12Wie verwaltest du den Zustand einer Feature-Seite ohne State-Bibliothek? Wo passen Smart- und Dumb-Komponenten hinein? Architektur
Das sagst du zuerst
Ein Signal-basierter Store-Service: private beschreibbare Signals, öffentliche schreibgeschützte Signals (
asReadonly()) undcomputed()-Selektoren, dazu Methoden mit Domänennamen, die unveränderliche Updates durchführen. Die geroutete (Smart-)Seite stellt den Store bereit und injiziert ihn; Dumb-Komponenten bekommen Daten über Inputs und melden Absichten über Outputs.Dann geh tiefer
- Scope nach Lebensdauer: Feature-Zustand in den
providersder Seite oder Route (stirbt mit ihr), globaler Zustand (User, Warenkorb, Theme) in einem Root-@Service(). - Server-Daten kommen in ein
httpResourceim Store; Zustand, der einen Refresh überleben oder teilbar sein muss, gehört in die URL (Query-Parameter +input()). - Eine Dumb-Komponente darf lokalen UI-Zustand haben und UI-Dinge injizieren (
ElementRef,DestroyRef), aber nie Daten-Services oder den Router — sonst lässt sie sich nicht leicht wiederverwenden oder testen, und eine Liste mit 50 Karten macht 50 Requests. - NgRx SignalStore lohnt sich, wenn das Team Konventionen, Plugins und Devtools will, nicht als Standard.
Das fragen sie wohl als Nächstes
Warum legst du schreibgeschützte Signals offen statt der beschreibbaren?
Warnsignal
Jeden Zustand in einen einzigen globalen Root-Service zu packen oder beschreibbare Signals offenzulegen, die jede Komponente direkt verändert.
- Scope nach Lebensdauer: Feature-Zustand in den
M-13Erkläre die DI-Hierarchie: Wann stellst du in root, auf einer Route oder an einer Komponente bereit, und wann brauchst du einen
InjectionToken? DIDas sagst du zuerst
inject()durchsucht die Element-Injectors (aktuelles Element, dann DOM-Parents), dann den Route-Injector, root und platform, und wirft NG0201, wenn nichts passt. Root (@Service()) bedeutet einen app-weiten Singleton;providersan einer Route eine Instanz pro Feature;providersan einer Komponente eine Instanz pro Komponente und ihre Kinder. Tokens sind für Werte, die keine Klassen sind: Konfiguration, URLs, Funktionen.Dann geh tiefer
@Service()(v22) entsprichtprovidedIn: 'root', ist tree-shakable und erlaubt nurinject().autoProvided: falsezwingt dich, ihn inprovidersaufzulisten.inject()schlägt Konstruktor-DI: keinesuper(…)-Ketten, es funktioniert in Guards, Interceptors und Factories und ermöglicht wiederverwendbareinjectX()-Funktionen. Es braucht einen Injection-Kontext (sonst NG0203).useClass,useValue,useFactory,useExistingundmulti: truefür Plugin-artige Arrays;optional,self,skipSelf,hostverändern die Suche.viewProviderssind für projizierten Inhalt unsichtbar;EnvironmentProviders(provideX()) gehören nur auf App- oder Route-Ebene, nicht in eine Komponente.
Das fragen sie wohl als Nächstes
Was passiert mit einem auf einer Lazy-Route bereitgestellten Service, wenn der Benutzer wegnavigiert?
Warnsignal
Einen Root-Service „sicherheitshalber“ in den
providerseiner Komponente aufzulisten — das erzeugt eine zweite Instanz und schwer auffindbare Zustandsfehler.M-14Funktionale Guards und Resolver: Was ist der Unterschied zwischen
canMatchundcanActivate, und wann würdest du einen Resolver vermeiden? RoutingDas sagst du zuerst
Guards sind einfache Funktionen, die
inject()nutzen.canMatchentscheidet, ob die Route für den Benutzer „existiert“ — wenn nicht, probiert der Router die nächste Route, und der Lazy-Chunk wird nicht geladen.canActivateblockiert den Zugang zu einer Route, die gematcht hat. Ein Resolver verzögert die Navigation, bis Daten da sind; für Listen und Dashboards bevorzuge ichhttpResourcein der Seite.Dann geh tiefer
- Gib
true/false, einenUrlTreeoder einenRedirectCommandzurück (Redirect + Navigationsoptionen wiereplaceUrl); seit v22.2 kannst du auch einenRedirectCommandwerfen. - Resolver: Die Seite erscheint mit fertigen Daten, aber die Navigation wartet, und ein Fehler bricht sie ab.
httpResource: sofortige Navigation, Laden und Fehler sind UI-Zustand. - Mit
withComponentInputBinding()kommen aufgelöste Daten, Params und Query-Params alsinput()s an. - Guards sind UX, keine Sicherheit: Die API muss die Autorisierung ebenfalls durchsetzen.
canDeactivateschützt ungespeicherte Änderungen.
Das fragen sie wohl als Nächstes
Wie würdest du auf demselben Pfad ein anderes Dashboard für Admins und normale Benutzer anzeigen?
Warnsignal
Klassenbasierte Guards mit Konstruktor-Injection als Standard oder einen Guard als einzige Zugriffskontrolle zu betrachten.
- Gib
M-15Schreibe einen funktionalen HTTP-Interceptor, der ein Auth-Token hinzufügt. Was sind die Stolperfallen? HTTP
Das sagst du zuerst
Ein
HttpInterceptorFnist eine Funktion(req, next), dienext(req)zurückgibt — oder einen Klon des Requests, denn Requests sind unveränderlich. Er wird mitprovideHttpClient(withInterceptors([…]))registriert und kanninject()nutzen.Dann geh tiefer
- Hänge das Token nur an deine eigene API (URL prüfen), nie an Drittanbieter-Hosts.
- Die Reihenfolge zählt: Requests durchlaufen die Interceptors in der angegebenen Reihenfolge, Responses kommen in umgekehrter Reihenfolge zurück.
- Rufe
inject()am Anfang der Funktion auf, nicht in RxJS-Callbacks wiecatchError, die später und außerhalb des Injection-Kontexts laufen. - Nutze
HttpContextTokenfür Metadaten pro Request (z. B. die Ladeleiste überspringen) statt falscher Header. AuchhttpResourceundvalidateHttplaufen über Interceptors.
export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = inject(AuthStore).token(); if (!token || !req.url.startsWith('/api')) return next(req); return next(req.clone({ setHeaders: { Authorization: 'Bearer ' + token } })); }; // app.config.ts provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]));Das fragen sie wohl als Nächstes
Wie würdest du ein 401 behandeln, indem du das Token einmal erneuerst und den ursprünglichen Request wiederholst?
Warnsignal
req.headersdirekt zu verändern oder das Token an jede URL zu schicken.M-16Wie strukturierst du die Fehlerbehandlung in einer Angular-App: HTTP-Fehler, Render-Fehler und alles andere? Architektur
Das sagst du zuerst
In Schichten: vorhersehbare HTTP-Fehler (404, 422) sind UI-Zustand im Store oder in der Komponente (
resource.error()); übergreifende (401, 5xx) gehören in einen Interceptor; ein defekter Bereich der Seite wird mit@boundaryisoliert; alles Unbehandelte landet in einem eigenen globalenErrorHandlerfür Logging und Telemetrie.Dann geh tiefer
@boundary(v22.2, Developer Preview) hat@error (let e = $error; let reset = $reset)-Blöcke, optional mitwhen-Bedingungen — der erste Treffer gewinnt.@boundaryfängt nur Render-Fehler ab, keine Fehler aus Event-Handlern oder asynchronem Code — die erreichen weiterhin denErrorHandler.provideBrowserGlobalErrorListeners()leitetwindow.onerrorund unbehandelte Rejections an denErrorHandlerweiter.- Ein Interceptor sollte nach seiner übergreifenden Reaktion den Fehler erneut werfen (
throwError(() => err)), damit auch der Aufrufer reagieren kann. Validierungsfehler sind keine Exceptions: Sie leben inerrors()des Formulars.
@boundary { <app-price-widget [price]="price()" /> <app-recommendations /> } @error (let e = $error; when e instanceof PermissionError) { <p>You don't have access to this widget.</p> } @error (let e = $error; let retry = $reset) { <app-error-card [message]="e.message" (retry)="retry()" /> }Das fragen sie wohl als Nächstes
Wo landet ein Fehler, der in einem Click-Handler geworfen wird?
Warnsignal
Ein
try/catchmitconsole.login jeder Komponente oder Fehler in einem Interceptor zu verschlucken, sodass sie sonst niemand sieht.M-17Wie funktioniert die Validierung in Signal Forms, einschließlich asynchroner Validierung, und wie unterscheidet sie sich von typisierten Reactive Forms? Formulare
Das sagst du zuerst
In Signal Forms ist das Modell ein einfaches Signal, und
form(model, schema)baut darüber einen Feldbaum mit Regeln, die in einer Schema-Funktion deklariert werden (required,email,validate,validateHttp…). Jeder Feldzustand ist ein Signal:valid(),errors(),pending(),touched(). Reactive Forms halten ihre eigene Kopie des Werts inFormControls und stellen Änderungen als Observables bereit.Dann geh tiefer
- Keine interne Kopie:
model.set()aktualisiert die Inputs, und Tippen aktualisiert das Modell — keinpatchValue/valueChanges. - Feldübergreifende Regeln nutzen
valueOf()invalidate; bedingte nutzenwhen,hidden,disabled(versteckte und deaktivierte Felder werden nicht validiert); wiederverwendbare Regeln nutzenschema()+apply(). validateHttpläuft überHttpClient(Interceptors greifen also) und hat in v22 einedebounce-Option;validateAsyncfunktioniert mit jeder Resource.pending()ist true, solange sie läuft.- Server-Fehler aus
submission.actionkönnen mit einemfieldTreezurückgegeben werden, damit sie beim richtigen Feld landen. - Die Migration kann schrittweise mit
SignalFormControlundcompatFormerfolgen; typisierte Reactive Forms (nonNullable,getRawValue()) bleiben für bestehenden Code gültig.
readonly model = signal({ username: '', password: '' }); readonly signup = form(this.model, (p) => { required(p.username); minLength(p.password, 8, { message: 'At least 8 characters' }); validateHttp(p.username, { request: ({ value }) => '/api/username-available?username=' + value(), debounce: 400, onSuccess: (res: { available: boolean }) => res.available ? undefined : { kind: 'taken', message: 'Username taken' }, onError: () => ({ kind: 'network', message: 'Could not verify' }), }); });Das fragen sie wohl als Nächstes
Wie würdest du validieren, dass das Enddatum nach dem Startdatum liegt?
Warnsignal
Einen asynchronen Validator zu schreiben, der bei jedem Tastendruck ohne Debounce einen Request abfeuert, oder nicht zu wissen, dass das Feld in der Zwischenzeit
pendingist.- Keine interne Kopie:
M-18Wie baust du ein eigenes Formular-Control (z. B. eine Sternebewertung), das mit Signal Forms funktioniert? Und mit Reactive Forms? Formulare
Das sagst du zuerst
Für Signal Forms implementierst du
FormValueControl<T>: Der einzige Pflichtmember istvalue = model(); danach funktioniert[formField]darauf genau wie auf eineminput. Für Reactive Forms ist der klassische WegControlValueAccessor, registriert mitNG_VALUE_ACCESSOR.Dann geh tiefer
- Optionale Inputs wie
disabled,invalid,touchedwerden vom Formular automatisch befüllt — deklariere nur, was du nutzt. In v22 meldet eintouch-Output, dass der Benutzer das Control berührt hat. - Boolesche Controls implementieren
FormCheckboxControlmitcheckedstattvalue. - Ein CVA braucht vier Methoden (
writeValue,registerOnChange,registerOnTouched,setDisabledState), und du musstonChangeselbst aufrufen; vergisst duonTouched, funktioniert „Fehler nach dem Berühren anzeigen“ nicht mehr. - Interop in v22: Signal Forms können CVAs binden, und Template-/Reactive-Forms unterstützen
FormValueControl, ein Control kann also während der Migration beide bedienen.
Das fragen sie wohl als Nächstes
Wie deaktiviert das Formular dein Control, und musst du das selbst behandeln?
Warnsignal
Ein
FormControlals Input an die Kindkomponente zu übergeben und es dort zu verändern.- Optionale Inputs wie
M-19Wie funktioniert
@defer, welche Trigger würdest du nutzen, und was sorgt dafür, dass ein Deferred-Block gar nicht wirklich lazy ist? PerformanceDas sagst du zuerst
@deferverlagert die Komponenten des Blocks in einen eigenen Chunk, der bei einem Trigger geladen wird:on idle(Standard),viewport,interaction,hover,timer,immediateoderwheneine Bedingung.prefetchlädt früher herunter, als gerendert wird. Lazy bleibt es nur, wenn diese Abhängigkeiten Standalone sind und sonst nirgends in der Datei verwendet werden.Dann geh tiefer
- Eine deferred Komponente in der Klasse (z. B.
viewChild(HeavyChart)) oder anderswo im Template zu referenzieren macht sie eager. @placeholderwird vor dem Trigger angezeigt (und ist das beobachtete Element fürviewport/interaction/hoverohne Ziel);@loading (after 100ms; minimum 500ms)vermeidet Flackern;@errorbehandelt einen fehlgeschlagenen Chunk-Download.whenfeuert nur einmal; mehrere Trigger werden mit ODER kombiniert.- Neu:
on viewport({ rootMargin: '200px' })(v21),on idle(500)(v22). Mit SSR liefernhydrate on …-Trigger inkrementelle Hydration, die in v22 standardmäßig aktiv ist.
@defer (on viewport; prefetch on idle) { <app-comments [postId]="post().id" /> } @placeholder (minimum 300ms) { <div class="comments-skeleton"></div> } @loading (after 100ms; minimum 500ms) { <app-spinner /> } @error { <p>We couldn't load the comments.</p> } @defer (on interaction; prefetch on hover) { <app-share-dialog /> } @placeholder { <button>Share</button> }Das fragen sie wohl als Nächstes
Wie würdest du prüfen, dass der Block wirklich in einem eigenen Chunk gelandet ist?
Warnsignal
Inhalte im sofort sichtbaren Bereich (Above the Fold) in
@deferzu packen oder zu glauben,@deferverzögere einen HTTP-Request statt das Laden von Code.- Eine deferred Komponente in der Klasse (z. B.
M-20Wie testest du in Angular 21+ mit Vitest eine Komponente mit Signal-Inputs und einen Service, der HTTP aufruft? Testing
Das sagst du zuerst
Komponenten:
TestBed.createComponent(Cmp, { bindings: [inputBinding('value', signal)] }), dannawait fixture.whenStable(), da es keine Zone zum Flushen gibt. HTTP:provideHttpClient()+provideHttpClientTesting(), dannHttpTestingController.expectOne(url).flush(data)undverify()inafterEach.Dann geh tiefer
- Vitest ist seit v21 der Standard-Runner (
@angular/build:unit-test); Karma ist deprecated. Nutzevi.fn()für Output-Spies undvi.useFakeTimers()stattfakeAsync. - Eine Änderung des gebundenen Signals aktualisiert den Input wie bei einem echten Parent — du brauchst kein
setInput(). - Signal-Stores lassen sich am einfachsten testen:
TestBed.inject(), Methoden aufrufen, aufcomputed()s prüfen. Effects laufen asynchron, also flushst du sie mitTestBed.tick(). - Teste Verhalten über das DOM oder Harnesses, nicht über private Felder.
TestBed.createDirective()(v22.2) testet eine Direktive ohne handgeschriebenen Host.
beforeEach(() => { TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] }); api = TestBed.inject(ProductsApi); http = TestBed.inject(HttpTestingController); }); afterEach(() => http.verify()); // no unexpected requests it('maps DTO → Product', async () => { const result = firstValueFrom(api.get(1)); http.expectOne('/api/products/1').flush({ id: 1, name: 'Mouse', price: 99, stock: 0 }); expect(await result).toEqual({ id: 1, name: 'Mouse', price: 99, inStock: false }); });Das fragen sie wohl als Nächstes
Wie würdest du eine Komponente testen, die
httpResourcenutzt?Warnsignal
In Unit-Tests das echte Backend anzusprechen oder wiederholt
detectChanges()aufzurufen und auf private Felder zu prüfen.- Vitest ist seit v21 der Standard-Runner (
Senior: Internals, Architektur, Trade-offs
Senior-Interviews prüfen, ob du für ein Team entscheiden kannst: wie das Framework darunter funktioniert, wie eine App mit 50 Entwicklern wartbar bleibt, was SSR, Sicherheit und Performance kosten. Antworte immer mit einer Position, dem Trade-off und damit, wie du sie in einem echten Projekt einführen würdest.
S-01Wie funktionieren Signals intern? Erkläre den Producer/Consumer-Graphen, Push-Pull und warum sie glitch-frei sind. Signals
Das sagst du zuerst
Signals bilden einen reaktiven Graphen, der aus Lesezugriffen aufgebaut wird:
signal()ist ein Producer,computed()ist beides, Effects und Templates sind aktive Consumer. Ein Schreibzugriff erhöht nur eine Version und schickt eine „möglicherweise veraltet“-Benachrichtigung; Werte werden erst beim Lesen gezogen und lazy neu berechnet. Eincomputedsieht daher immer konsistente Eingaben und läuft pro Schreib-Batch nur einmal.Dann geh tiefer
- Abhängigkeiten sind dynamisch: Bei jedem Lauf wird die Producer-Liste aus dem aufgebaut, was tatsächlich gelesen wurde. Ein Signal, das nur in einem inaktiven
if-Zweig gelesen wird, löst deshalb nichts aus. - Gleichheit stoppt die Propagierung: Die Version steigt nur, wenn der neue Wert nicht gleich ist (standardmäßig
Object.is). Eincomputed, dessen Ergebnis sich nicht ändert, weckt seine Consumer nicht;{ equal }passt das für Werte an, die vom Server neu erzeugt werden. - Konsequenz: Mutation (
items().push()) benachrichtigt niemanden, weil Referenz und Version gleich bleiben. Immutability ist kein Stilthema, sondern die Art, wie der Graph Änderungen erkennt. - Effects sind asynchron und werden zusammen mit der Change Detection eingeplant (View-Effects vor der Prüfung ihrer Komponente, Root-Effects vor allen Komponenten). Signals sind kein Event-Mechanismus und ordnen Seiteneffekte nicht zeitlich — das ist das Terrain von RxJS.
const a = signal(1); const b = signal(2); const sum = computed(() => a() + b()); const product = computed(() => a() * b()); const result = computed(() => sum() + product()); // diamond a.set(5); b.set(3); // push: versions bump, nothing is recalculated yet result(); // pull: sum, product, result run once each, with consistent values // equality cutoff: a new array with the same content does not propagate const ids = signal<number[]>([], { equal: (x, y) => x.length === y.length && x.every((v, i) => v === y[i]), });Das fragen sie wohl als Nächstes
Warum ist ein
effect(), der in ein anderes Signal schreibt, meist das falsche Werkzeug, und was nimmst du stattdessen?Warnsignal
„Ein Signal ist nur ein BehaviorSubject mit schönerer API“ oder „
set()rendert die Komponente synchron neu“ — das zeigt, dass Laziness, Versionen und Push-Pull nicht verstanden wurden.- Abhängigkeiten sind dynamisch: Bei jedem Lauf wird die Producer-Liste aus dem aufgebaut, was tatsächlich gelesen wurde. Ein Signal, das nur in einem inaktiven
S-02Was plant in einer Zoneless-Anwendung mit OnPush-Komponenten eigentlich die Change Detection ein, und was überspringt Angular? Change Detection
Das sagst du zuerst
Ohne Zone.js patcht niemand
setTimeoutoder Promises, daher rendert Angular nur, wenn es Bescheid bekommt: Ein im Template gelesenes Signal ändert sich, ein Template-/Host-Listener feuert, dieasync-Pipe emittiert,markForCheck(),setInput()oder eine Navigation. Ein OnPush-Teilbaum wird übersprungen, solange ihn keines davon markiert. Der Aufwand skaliert also mit dem, was sich geändert hat, nicht mit der Größe des Baums.Dann geh tiefer
- Das Template ist ein reaktiver Consumer: Ändert sich ein gelesenes Signal, markiert Angular diese View zur Aktualisierung (wie
markForCheck), und die Change Detection erreicht nur die betroffenen Komponenten. - OnPush vergleicht Inputs per Referenz (
===): Wird ein per Input übergebenes Objekt mutiert, aktualisiert sich das Kind nicht — dieselbe Immutability-Regel wie bei Signals. - Der klassische Bug: ein einfaches Feld, das in
setTimeout,subscribe()oder einem Callback einer Drittbibliothek (WebSocket, Maps) gesetzt wird — „es aktualisiert sich erst, wenn ich irgendwo klicke“. Die Lösung ist ein Signal odertoSignal();markForCheck()ist der Notausgang für Legacy-Code. detectChanges()ist synchron und nur selten nötig (Messungen, Tests);markForCheck()ist die empfohlene asynchrone Variante.
// ✗ worked with zone.js, does nothing zoneless + OnPush export class Clock { now = new Date(); constructor() { setInterval(() => (this.now = new Date()), 1000); } } // ✓ the signal tells Angular what changed export class Clock { readonly now = signal(new Date()); constructor() { const id = setInterval(() => this.now.set(new Date()), 1000); inject(DestroyRef).onDestroy(() => clearInterval(id)); } }Das fragen sie wohl als Nächstes
Ein OnPush-Kind erhält ein Objekt per Input, und der Parent mutiert es direkt. Was passiert, und wie löst du das auf Teamebene?
Warnsignal
detectChanges()überall verstreuen, bis der Bildschirm sich aktualisiert, oder glauben, Zoneless heiße „Angular prüft seltener, ist also fehleranfälliger“.- Das Template ist ein reaktiver Consumer: Ändert sich ein gelesenes Signal, markiert Angular diese View zur Aktualisierung (wie
S-03Wie würdest du eine große, Zone.js-basierte Anwendung auf Zoneless migrieren, ohne die Feature-Entwicklung zu stoppen? Change Detection
Das sagst du zuerst
Inkrementell: Zuerst machst du die Komponenten unter OnPush korrekt, mit dem Zustand in Signals, solange Zone.js noch als Sicherheitsnetz da ist, und entfernst Zone.js dann, wenn nichts mehr davon abhängt. Das Risiko ist nicht Angular, sondern Code und Bibliotheken, die sich stillschweigend darauf verlassen haben, dass Zone.js Renderings auslöst.
Dann geh tiefer
- Zuerst die Bestandsaufnahme: einfache Felder, die in asynchronen Callbacks gesetzt werden,
subscribe()in Felder, Bibliotheken, die aufNgZone.onStablewarten. Angular 21 liefert ein Analysewerkzeug mit (onpush_zoneless_migration, über den MCP-Server der CLI,ng mcp). - Migriere zuerst die Blattkomponenten, Feature für Feature:
Eagerentfernen, Zustand in Signals /toSignal()verschieben, Review und Tests. Jeder PR ist klein und rückgängig zu machen. - Bis zum Schluss behältst du
provideZoneChangeDetection({ eventCoalescing: true })und das Zone.js-Polyfill; danach entfernst du beides und achtest auf Bugs vom Typ „aktualisiert sich erst nach einem Klick“. - Auch die Tests ändern sich:
fakeAsyncbraucht Zone.js, also wechselst du aufawait fixture.whenStable()und die Fake Timers von Vitest. - Im Team: eine Lint-/Review-Regel, dass neuer Code Signals für den Template-Zustand nutzt, und eine kurze Checkliste im PR-Template, damit der migrierte Bereich nicht zurückfällt.
Das fragen sie wohl als Nächstes
Eine Charting-Bibliothek eines Drittanbieters aktualisiert Daten in ihren eigenen Callbacks. Wie bindest du sie ohne Zone.js ein?
Warnsignal
„Wir entfernen Zone.js in einem großen PR und beheben in der QA, was kaputtgeht“ — kein Inventar, kein Sicherheitsnetz, kein Plan für Bibliotheken und Tests.
- Zuerst die Bestandsaufnahme: einfache Felder, die in asynchronen Callbacks gesetzt werden,
S-04Wie entwirfst du die State-Schicht einer großen Anwendung: Service mit Signals, NgRx SignalStore oder klassischer NgRx Store? Architektur
Das sagst du zuerst
Die erste Frage lautet nicht „welche Bibliothek“, sondern wo jedes Stück Zustand lebt und wem es gehört. Ein Service mit privaten Signals,
computed()-Selektoren und klar benannten Methoden deckt die meisten Anwendungen ab; SignalStore, wenn du eine vorgegebene Struktur, Entities und wiederverwendbare Features über viele Teams hinweg willst; der klassische NgRx Store nur, wenn du wirklich ein Action-Log, Time-Travel oder eventgetriebene Abläufe brauchst.Dann geh tiefer
- Halte den Zustand auf der niedrigsten Ebene, die ihn braucht (Komponente → Feature → global), und hebe ihn erst an, wenn ein neuer Consumer auftaucht. Der Zustand einer Seite wird auf der Route oder der Komponente bereitgestellt, nicht in root, damit er zwischen Besuchen nicht „kleben bleibt“.
- Serverdaten sind ein Cache, nicht dein Zustand: Du hältst sie in
httpResource/resourceund lädst nach Schreibzugriffen neu. Auch die URL ist Zustand (Filter, Paginierung). - Regeln, die skalieren: schreibgeschützte Signals nach außen geben (
asReadonly()), eine einzige Quelle der Wahrheit mit abgeleitetem Rest, keine Stores, die sich zirkulär gegenseitig injizieren — das deutet meist auf eine fehlende Facade oder ein falsch platziertes Konzept hin. - Beim SignalStore rufen nur Methoden
patchStateauf,rxMethodübernimmt asynchrone Abläufe (mitcatchErrorim inneren Stream), undsignalStoreFeaturebeseitigt kopierten Loading-/Error-Code. Die Bibliotheken können neben einem bestehenden NgRx Store koexistieren.
Das fragen sie wohl als Nächstes
Zwei Features brauchen dieselben Warenkorb-Daten. Wo lebt der Store, und wie kommunizieren sie, ohne einander zu importieren?
Warnsignal
„Bei allem Ernsthaften immer NgRx“ oder „ein einziger globaler Store für die ganze Anwendung“ — das Werkzeug wird gewählt, bevor Besitz und Lebensdauer des Zustands verstanden sind.
S-05Wie strukturierst du eine große Codebasis in Feature- und Shared-Code, und wie erzwingst du die Grenzen in einem Monorepo? Architektur
Das sagst du zuerst
Organisiere nach Domäne, nicht nach Dateityp: Features hängen von Core und Shared ab, Shared hängt von nichts Domänenspezifischem ab, und Features importieren einander nie direkt. Regeln, die nur in einem Wiki stehen, werden in der ersten Woche gebrochen, deshalb lege ich sie ins Lint —
no-restricted-importsin einer einzelnen Anwendung oder Nx-Tags mit@nx/enforce-module-boundariesin einem Monorepo.Dann geh tiefer
- Tags auf zwei Dimensionen: Scope (wer mit wem sprechen darf:
scope:shop,scope:shared) × Type (Richtung der Abhängigkeiten: Feature → UI / Data-Access → Util). Die Constraints aus beiden gelten per UND. - Eine öffentliche API pro Feature/Bibliothek (ein einziges Barrel an der Grenze, nie in jedem Ordner); Deep Imports und statische Imports von Lazy-Bibliotheken sind Verstöße.
- Die Granularität ist ein Kompromiss: Kleine Bibliotheken nach Domäne und Typ ergeben schmale
nx affected-Graphen und Cache-Treffer; eine riesige Shared-Bibliothek sorgt dafür, dass jeder PR das ganze Repo betrifft. - In Shared landet nur, was generisch, stabil und bereits in mehreren Features im Einsatz ist („Rule of Three“); Domänenkomponenten, die auch anderswo gebraucht werden, bleiben im besitzenden Feature und werden über dessen öffentliche API exportiert.
'@nx/enforce-module-boundaries': ['error', { depConstraints: [ // scope: who may talk to whom { sourceTag: 'scope:shop', onlyDependOnLibsWithTags: ['scope:shop', 'scope:shared'] }, { sourceTag: 'scope:shared', onlyDependOnLibsWithTags: ['scope:shared'] }, // type: which way dependencies flow { sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:feature', 'type:ui', 'type:data-access', 'type:util'] }, { sourceTag: 'type:ui', onlyDependOnLibsWithTags: ['type:ui', 'type:util'] }, { sourceTag: 'type:util', onlyDependOnLibsWithTags: ['type:util'] }, // ui never reaches data-access, not even transitively { sourceTag: 'type:ui', notDependOnLibsWithTags: ['type:data-access'] }, ], }],Das fragen sie wohl als Nächstes
Ein Entwickler „behebt“ einen Lint-Fehler mit
sourceTag: '*'. Was tust du, und wie verhinderst du eine Wiederholung?Warnsignal
Ordner nach Typ (
components/,services/) und einsharedals Sammelschublade, mit Grenzen, die „mündlich vereinbart“ sind.- Tags auf zwei Dimensionen: Scope (wer mit wem sprechen darf:
S-06Wie würdest du Ports & Adapters in Angular anwenden? Wo lebt HTTP, und wann lohnt es sich nicht? Architektur
Das sagst du zuerst
Die Domäne ist reines TypeScript, Use Cases hängen von Ports ab, die als abstrakte Klassen geschrieben sind (die zugleich DI-Tokens sind), und HTTP lebt nur in Adaptern am Rand, die DTOs auf Entities abbilden. Die einzige Stelle, die einen Port an einen Adapter bindet, ist die Composition Root —
app.config.tsoder die Provider einer Route. Für einfache CRUD-Screens ist das Overkill.Dann geh tiefer
- Abstrakte Klasse statt Interface: Ein Interface verschwindet beim Kompilieren, du bräuchtest also einen separaten
InjectionToken; die abstrakte Klasse ist zugleich Typ und Token. - Fehler gehören zum Vertrag: Der Adapter übersetzt Timeouts/503 in einen Domänenfehler, sodass keine Komponente je einen
HttpErrorResponsezu sehen bekommt. - Der Gewinn zeigt sich beim Testen: Domänenregeln als reine Funktionen, Use Cases mit einem In-Memory-Adapter und eine einzige Contract-Test-Suite, die gegen jeden Adapter läuft.
- Pragmatische Variante: eine reine Domäne für die Regeln, die zählen, ein Port nur dort, wo es mehrere Implementierungen gibt (oder geben wird), der Rest in der üblichen Data-Access-/UI-Struktur — und die Schichten per Lint erzwingen.
// application/task-repository.ts — the PORT (type + DI token) export abstract class TaskRepository { abstract list(): Promise<readonly Task[]>; abstract save(task: Task): Promise<void>; } // infrastructure/http-task-repository.ts — the ADAPTER (HttpClient + DTO mapping) @Injectable() export class HttpTaskRepository extends TaskRepository { /* … */ } // app.config.ts — the COMPOSITION ROOT providers: [{ provide: TaskRepository, useClass: HttpTaskRepository }]Das fragen sie wohl als Nächstes
Warum ist
@Injectable({ providedIn: 'root', useClass: HttpTaskRepository })auf dem Port eine schlechte Idee?Warnsignal
Komponenten, die
HttpClientoder den konkreten Adapter direkt injizieren, DTOs, die in Templates fließen, oder vier Schichten für zwei Formulare „weil Clean Architecture“.- Abstrakte Klasse statt Interface: Ein Interface verschwindet beim Kompilieren, du bräuchtest also einen separaten
S-07Wann würdest du dich für Micro-Frontends mit Native Federation entscheiden, und wann würdest du dagegenhalten? Architektur
Das sagst du zuerst
Micro-Frontends lösen ein organisatorisches Problem — autonome Teams, die unabhängig deployen müssen und sich heute beim Release gegenseitig blockieren —, kein technisches. Kann ich keine zwei Teams nennen, die sich blockieren, empfehle ich eine einzige Anwendung in einem Monorepo mit Lazy Routes und erzwungenen Grenzen; MFE verschlechtert die Performance und verschiebt verworrenen Code nur zwischen Repositories.
Dann geh tiefer
- Gültige Gründe: viele Teams mit unterschiedlichen Release-Rhythmen, sehr große Organisationen, eine Strangler-Fig-Migration weg von einer Legacy-Anwendung, getrennte Produkte unter einer gemeinsamen Experience.
- Angular muss ein Singleton mit
strictVersionsein, idealerweise überall exakt dieselbe Version: Zwei Runtimes bedeuten Komponenten, die nicht in den Baum der Shell passen, und Services mit zwei Instanzen. Ein Remote mit einer anderen Major-Version wird als Web Component oder iframe isoliert. - Behandle die Exposes jedes Remotes als öffentliche API mit SemVer; kommuniziere über die URL, dann über typisierte Events, dann über einige geteilte Singleton-Services — nie über einen gemeinsamen globalen Store (das ist ein verteilter Monolith).
- Plane die Kosten ab dem ersten Tag ein: Timeout und Fallback pro Remote, ein Manifest pro Umgebung, Name und Version des Remotes in jedem geloggten Fehler, E2E auf der Produktionskombination.
Das fragen sie wohl als Nächstes
Ein Remote lässt sich in Produktion nicht laden. Wie soll sich die Shell verhalten — bei einer ganzen Route im Vergleich zu einem Widget innerhalb einer Seite?
Warnsignal
„Wir brauchen Micro-Frontends für Skalierbarkeit und Performance“ oder
strictVersion: falsesetzen, damit die Versionswarnungen verschwinden.S-08Wann ist Angular Elements das richtige Werkzeug, und welche Stolperfallen gibt es, wenn man eine Angular-Komponente als Custom Element ausliefert? Komponenten
Das sagst du zuerst
Wenn eine Komponente außerhalb einer Angular-Anwendung leben muss: CMS-Widgets, Kundenwebsites, ein Design System für andere Frameworks oder ein Remote, das die Angular-Version nicht teilen kann.
createCustomElement()macht aus Inputs Attribute/Properties und aus OutputsCustomEvents, aber jedes Bundle bringt seine eigene Runtime mit (≈ 33 kB übertragen), und es gibt keine gemeinsame DI — innerhalb derselben Angular-Anwendung ist es fast immer die falsche Wahl.Dann geh tiefer
- Attribute sind immer Strings: Numerische/boolesche Inputs brauchen
numberAttribute/booleanAttribute, und Objekte/Arrays gehen nur über Properties. - Events bubblen nicht und sind nicht composed: Höre direkt auf dem Element. Verwende spezifische Output-Namen (
ratingChange, nichtchange). - Nutze
ViewEncapsulation.ShadowDom, damit das CSS der Host-Seite nicht hineinwirkt; Theming läuft über CSS-Variablen und::part(). - Fasse die Widgets eines Teams in einem Bundle zusammen (ein
createApplication(), mehreredefine()-Aufrufe), verwende einen Tag, der sich vom Selektor der Komponente unterscheidet, und verwende ein zerstörtes Element nicht wieder — erzeuge ein neues.
Das fragen sie wohl als Nächstes
Drei Teams liefern jeweils ein separat gebautes Widget auf derselben Seite aus. Was kostet das, und was würdest du ändern?
Warnsignal
Angular Elements nutzen, um Komponenten zwischen Screens derselben Angular-Anwendung wiederzuverwenden, oder für SEO-kritische Inhalte, die auf dem Server gerendert werden sollten.
- Attribute sind immer Strings: Numerische/boolesche Inputs brauchen
S-09Wie entscheidest du in einer Angular-Anwendung zwischen CSR, SSR und Prerender, und was kostet der Betrieb? SSR
Das sagst du zuerst
Pro Route, nicht pro Projekt:
app.routes.server.tslegt für jeden PfadRenderMode.Prerender,ServeroderClientfest. Stabile öffentliche Seiten werden prerendert, öffentliche Seiten mit aktuellen Daten auf dem Server gerendert, und der Bereich hinter dem Login bleibt auf dem Client. SSR verbessert LCP und SEO; es verkleinert das JavaScript nicht und repariert keine langsame Anwendung nach der Hydration.Dann geh tiefer
- Prerender mit Parametern braucht
getPrerenderParams; mitPrerenderFallback.Server(Standard) kannst du die Top-N-Produkte prerendern und den Rest on demand rendern. Jede URL ist zur Build-Zeit ein vollständiges Rendering. - Der Server-Modus bedeutet einen Node-Prozess: Die TTFB beträgt mindestens so viel wie die langsamste API, die beim Rendern aufgerufen wird, dazu kommen CPU pro Request und Zustand auf Modulebene, der zwischen Nutzern geteilt wird.
- Korrekte Statuscodes zählen für SEO: Setze 404 über
RESPONSE_INITstatt eines Soft-404; das Caching gilt pro Route —public, s-maxagenur für identische Inhalte,private, no-storefür personalisiertes HTML. - In v22 konfigurierst du
allowedHosts(oderNG_ALLOWED_HOSTS): Ein unbekannter Host erhält 400 als SSRF-Schutz. Kann niemand einen Node-Server betreiben, nimm Prerender mitoutputMode: 'static'.
Das fragen sie wohl als Nächstes
Deine SSR-Seiten sind langsam geworden, nachdem eine Backend-API schlechter wurde. Warum, und was änderst du?
Warnsignal
„SSR überall einschalten, das macht die App schneller“ — und dabei ignorieren, dass sich die TTI kaum ändert, was der Server kostet und dass personalisierte Seiten öffentlich im CDN gecacht werden.
- Prerender mit Parametern braucht
S-10Erkläre inkrementelle Hydration und Event Replay in v22. Wie setzt du sie auf einer Produktseite ein? SSR
Das sagst du zuerst
In v22 aktiviert ein schlichtes
provideClientHydration()die inkrementelle Hydration und damit auch Event Replay.@defer (hydrate on …)-Blöcke werden auf dem Server gerendert, bleiben im Browser aber dehydriert — ihr JS wird nicht einmal heruntergeladen —, bis der Trigger feuert; Klicks davor werden in eine Queue gestellt und wiedergegeben, und ein Klick in einen dehydrierten Block hydriert ihn.Dann geh tiefer
- Anders als bei einem normalen
@deferist der endgültige Inhalt bereits auf dem Bildschirm, du kannst also auch oberhalb des Folds verzögern, ohne Layout Shift. hydrate neverist nur für wirklich statische Inhalte gedacht: Er wird nicht annotiert, also wird nichts wiedergegeben. Behalte den@placeholder: Hydrate-Trigger werden bei clientseitiger Navigation ignoriert.withIncrementalHydration()ist deprecated undwithEventReplay()ist gegenüber dem Standard redundant; das Opt-out istwithNoIncrementalHydration().- Verpacke keine Komponenten von wenigen KB: Der zusätzliche Chunk und Request kosten mehr, als sie sparen. Nutze CSS-Zustände (
:active,:focus-visible), damit Nutzer sehen, dass der Klick registriert wurde.
<app-product-header [product]="product()" /> <!-- hydrated at bootstrap --> @defer (hydrate on interaction) { <app-gallery [images]="product().images" /> <!-- JS only at the first click --> } @placeholder { <div class="gallery-skeleton"></div> <!-- shown on client-side navigation --> } @defer (hydrate on viewport) { <app-reviews [productId]="id()" /> <!-- below the fold --> } @placeholder { <div class="reviews-skeleton"></div> } @defer (hydrate never) { <app-footer /> } <!-- static HTML forever -->Das fragen sie wohl als Nächstes
Was verlangt
hydrate whenvom Parent, und was passiert bei verschachtelten dehydrierten Blöcken?Warnsignal
hydrate neverauf etwas mit Listenern setzen oder@placeholderentfernen, „weil man ihn ohnehin nie sieht“.- Anders als bei einem normalen
S-11Welcher Code ist nicht SSR-sicher, und wie verhinderst du Hydration-Mismatches in einem Team? SSR
Das sagst du zuerst
Alles, was in Konstruktoren, Feld-Initialisierern,
ngOnInitodereffect()window,localStorage, Layout oder Observer anfasst — das läuft auch auf dem Server. Reine Browser-Arbeit gehört inafterNextRender/afterRenderEffectoder hinter einen Plattform-Provider, und die gerenderte Ausgabe muss auf beiden Plattformen identisch sein.Dann geh tiefer
@if (isPlatformBrowser(…))in einem Template wirft keinen Fehler, aber Angular verwirft das Server-DOM für diesen Block und baut es neu auf — ein Layout Shift. Bevorzuge@deferoderafterNextRender.- Ursachen für Mismatches: direkte DOM-Manipulation (
innerHTML,appendChild), ungültiges HTML (<div>in<p>), Skripte von Drittanbietern, ein CDN, das Kommentare entfernt (NG0507), und Werte wieDate.now(), die sich zwischen den Plattformen unterscheiden. - Zoneless-Server sehen dein eigenes Async nicht (fetch, SDKs, Timer): Nutze
resource()oderPendingTasks, sonst geht das HTML leer raus und der Browser fordert die Daten erneut an. - State Leaks: Variablen auf Modulebene und
useValue-Objekte werden von allen Nutzern des Node-Prozesses geteilt; nutze Services oderuseFactory. - Im Team: ein SSR-Build in der CI, eine Review-Checkliste, und
ngSkipHydrationnur als dokumentierter, temporärer Patch erlaubt.
Das fragen sie wohl als Nächstes
Die Theme-Präferenz liegt in
localStorage. Wie renderst du auf dem Server das richtige Theme?Warnsignal
if (typeof window !== 'undefined')quer durch die Komponenten gestreut, odereffect()als „Browser-only-Hook“ behandeln.S-12Du übernimmst eine langsame Angular-Anwendung. Wie sieht deine Performance-Strategie aus, und wie verhinderst du Regressionen? Performance
Das sagst du zuerst
Erst messen, dann den größten Engpass beheben: Core Web Vitals (LCP, INP, CLS) von echten Nutzern und der Angular-DevTools-Profiler zeigen mir, ob das Problem beim Laden, beim Rendern oder an einem ausgelasteten Main Thread liegt. Danach sichere ich die Gewinne mit Bundle Budgets in der CI ab.
Dann geh tiefer
- Laden: Lazy Routes,
@deferfür schwere Inhalte unterhalb des Folds (nie das LCP-Element),injectAsync()für schwere, selten genutzte Services,NgOptimizedImagemitpriorityauf dem LCP-Bild, gezielte Imports, geprüft mitsource-map-explorer. - Rendern: OnPush + Signals,
trackauf einer stabilen ID, Ableitungen incomputed()statt Template-Methoden; bei Zehntausenden Zeilen CDK Virtual Scroll oder Paginierung — OnPush reduziert, wie oft, nicht wie viel du renderst. - Main Thread: Dauerberechnungen über ~50 ms (Parsing, große Filter) gehören in einen Web Worker; übertrage
ArrayBuffers, statt riesige Datenmengen zu kopieren. - Regressionskontrolle:
budgetsin derangular.json, die den Build fehlschlagen lassen, eine Performance-Prüfung der Schlüsselseiten pro Release und jemand, der für die Zahlen verantwortlich ist.
Das fragen sie wohl als Nächstes
INP ist schlecht, aber LCP ist in Ordnung. Wo schaust du zuerst hin?
Warnsignal
Optimieren ohne Messung oder „überall SSR und
@defer“ — auch auf dem Hauptinhalt, was den LCP verschlechtert.- Laden: Lazy Routes,
S-13Wie schützt Angular vor XSS, wo sind die Seitentüren, und was ist deine zweite Verteidigungslinie? Sicherheit
Das sagst du zuerst
Angular behandelt jeden gebundenen Wert als unsicher: Interpolation escaped,
[innerHTML]wird sanitisiert, gefährliche URL-Schemata werden zuunsafe:. XSS in Angular-Anwendungen entsteht fast immer durch das Umgehen dieses Schutzes —bypassSecurityTrust*auf nicht vertrauenswürdigen Daten,nativeElement.innerHTML,eval—, daher ist die zweite Linie ein strenges CSP mit Nonce plus Trusted Types.Dann geh tiefer
bypassSecurityTrust*ist nur für Inhalte legitim, die du selbst baust und kontrollierst (eine Embed-URL aus einer validierten ID). Eine generische „safeHtml“-Pipe ist ein wiederverwendbares XSS; suche in jedem Review nach diesen Aufrufen.- CSP: Angular braucht für seine Inline-Styles einen Nonce —
ngCspNonceam Root-Element oder dasCSP_NONCE-Token;security.autoCspberechnet Hashes zur Build-Zeit, deckt aber nur Skripte ab und funktioniert nicht mit SSR. - Führe CSP zuerst mit
Content-Security-Policy-Report-Onlyein, sonst brechen Lazy Chunks oder Inline-Styles in Produktion. - CSRF bei Session-Cookies:
HttpClientsendet das CookieXSRF-TOKENalsX-XSRF-TOKENbei verändernden Same-Origin-Requests — aber der Server muss es setzen und prüfen. Jede Frontend-Prüfung wird auf dem Server wiederholt.
Das fragen sie wohl als Nächstes
Product möchte von Nutzern geschriebenes Markdown rendern. Wie machst du das sicher?
Warnsignal
„Angular sanitisiert alles, also sind wir sicher“, während die Codebasis eine Pipe mit
bypassSecurityTrustHtmlauf CMS-Inhalten enthält.S-14Entwirf einen Auth-Interceptor mit Refresh-Tokens. Was passiert, wenn sechs Requests gleichzeitig ein 401 erhalten, und wo speicherst du die Tokens? HTTP
Das sagst du zuerst
Genau ein Refresh in flight, geteilt von allen wartenden Requests (ein gecachtes Observable mit
shareReplay(1), infinalizegeleert), danach wird jeder ursprüngliche Request mit dem neuen Token wiederholt. Access Token im Speicher, Refresh Token in einem HttpOnly-, Secure-, SameSite-Cookie — oder ein BFF, bei dem der Browser nur ein Session-Cookie hat.Dann geh tiefer
- Das naive „Refresh bei jedem 401“ erzeugt N Refreshes; bei Refresh-Token-Rotation machen sie sich gegenseitig ungültig, und manche Server werten die Wiederverwendung als Angriff und beenden die Session.
- Markiere Login/Refresh mit einem
HttpContextToken(SKIP_AUTH), damit der Interceptor sich nicht selbst abfängt, hänge das Token nur an die URLs deiner API, und wenn sich das Token seit dem Senden des Requests schon geändert hat, wiederhole einfach. - Nur 401 löst einen Refresh aus — 403 heißt „nicht erlaubt“. Ein fehlgeschlagener Refresh bedeutet Logout, Leeren von Stores und Caches und Weitergabe des Logouts an andere Tabs (
BroadcastChannel). - Beim Start versucht ein
provideAppInitializereinen Refresh vor der ersten Route. Mit einem externen OIDC-Provider nimmst du eine zertifizierte Bibliothek, statt PKCE und Rotation selbst zu bauen.
private refreshInFlight: Observable<string> | null = null; refresh(): Observable<string> { this.refreshInFlight ??= this.http .post<{ accessToken: string }>('/auth/refresh', null, { withCredentials: true, // HttpOnly cookie context: new HttpContext().set(SKIP_AUTH, true), // not intercepted again }) .pipe( map((r) => r.accessToken), tap((t) => this.token.set(t)), // access token: memory only finalize(() => (this.refreshInFlight = null)), shareReplay(1), // every 401 waits for the same one ); return this.refreshInFlight; }Das fragen sie wohl als Nächstes
Warum ist
shareReplay(1)hier akzeptabel, obwohl es bei anderen Streams eine bekannte Stolperfalle ist?Warnsignal
JWT in
localStorageohne Gedanken an XSS oder CSP, ein separater Refresh pro 401 oder Berechtigungsprüfungen auf Basis des im Frontend dekodierten JWT.S-15Wie sieht deine Strategie zur Fehlerbehandlung in einer Angular-Anwendung aus? Architektur
Das sagst du zuerst
In Schichten: Vorhersehbare Fehler werden lokal bei den Daten behandelt (ein 404 oder 422, den Store/Komponente anzeigt), übergreifende zentral in einem Interceptor (401, 5xx), Rendering-Fehler eines Bereichs mit
@boundary(v22.2, Developer Preview), und alles andere landet in einem globalenErrorHandler, der in die Telemetrie loggt.Dann geh tiefer
provideBrowserGlobalErrorListeners()leitetwindow.onerrorund unbehandelte Rejections an denErrorHandlerweiter; halte den Handler einfach und sicher — wirft er selbst, verlierst du jeden Fehler.@boundaryfängt nur Rendering-Fehler, keine aus Event-Handlern oder asynchronem Code, und ein gefangener Fehler wird trotzdem an denErrorHandlergemeldet.- Der Interceptor wirft nach der Behandlung erneut (
throwError(() => err)), damit Aufrufer weiterhin reagieren können; eincatchError(() => of([]))ohne Logging macht Bugs unsichtbar. - UX und Betrieb: Toasts zentralisieren und deduplizieren (nicht fünf Meldungen für ein Problem), technische Fehler im Adapter in Domänenfehler übersetzen und jedem geloggten Fehler Kontext (Route, Version, Remote) mitgeben.
Das fragen sie wohl als Nächstes
Ein Widget im Dashboard wirft beim Rendern und leert die ganze Seite. Wie begrenzt du den Schaden?
Warnsignal
Ein
try/catchodercatchErrorin jeder Komponente, das Fehler verschluckt, und ein Toast für jeden davon, ohne zentrales Logging.S-17Welche Teststrategie würdest du für ein Team festlegen: Was gehört in Unit-, Komponenten- und E2E-Tests, und was mockst du? Testing
Das sagst du zuerst
Teste Verhalten, nicht Implementierung, und verschiebe jede Prüfung auf die günstigste Ebene, die sie abfangen kann: reine Funktionen und Stores ohne DOM, Dumb-Komponenten über ihre Inputs und Outputs, Smart-Seiten mit einem Fake-Store und
HttpTestingControllerund einige wenige E2E-Abläufe (Playwright/Cypress) für das, was erst die Integration offenlegt. Gemockt wird an den Grenzen — HTTP und Ports —, nicht an den eigenen Kindkomponenten.Dann geh tiefer
- Seit v21 ist Vitest der Standard-Runner (
@angular/build:unit-test); Karma ist deprecated, und alte Suites lassen sich mit dem experimentellen Schematicrefactor-jasmine-vitestumziehen. - Zoneless-Tests:
await fixture.whenStable()stattfakeAsync, Vitest Fake Timers für Debounce,TestBed.tick(), um Effects auszuführen; Inputs als Signals überinputBinding. - Bei Ports & Adapters lässt du eine einzige Contract-Test-Suite gegen jeden Adapter laufen; CDK-Harnesses machen Komponententests unabhängig von der DOM-Struktur.
- In der CI:
nx affectedplus Cache für die Geschwindigkeit, kein Cache für nicht deterministische E2E-Tests gegen ein echtes Backend, und Coverage als Signal, nicht als Ziel.
Das fragen sie wohl als Nächstes
Ein instabiler E2E-Test schlägt bei einem von zehn Läufen fehl. Wie gehst du im Team damit um?
Warnsignal
„100 % Coverage bei komplett gemocktem Code“ — Tests, die prüfen, ob private Methoden aufgerufen wurden, bei jedem Refactoring brechen und echte Bugs übersehen.
- Seit v21 ist Vitest der Standard-Runner (
S-18Welche i18n-Strategie würdest du wählen: das Build-Time-i18n von Angular oder eine Übersetzungsbibliothek zur Laufzeit? i18n
Das sagst du zuerst
Standardmäßig das eingebaute
@angular/localize: Texte, die miti18n/$localizemarkiert sind, mitng extract-i18nextrahiert, und ein optimierter Build pro Locale ohne Key-Lookup zur Laufzeit. Eine Laufzeitbibliothek nur, wenn der Sprachwechsel ohne Reload eine Produktanforderung ist oder Übersetzungen ohne Deployment aus einem CMS kommen.Dann geh tiefer
- Die Sprache mit dem eingebauten i18n zu wechseln bedeutet einen Reload, auch mit
loadTranslations()zur Laufzeit, weil Template-Texte beim Erzeugen der Komponenten ausgewertet werden. - Plurale brauchen ICU, nicht
n === 1 ? … : …: Rumänisch hatone,fewundother(„20 de fișiere“). Setze Sätze nie durch Konkatenation zusammen — die Wortstellung unterscheidet sich zwischen Sprachen. - Eigene IDs (
@@cart.title), damit Übersetzungen Änderungen am Quelltext überstehen; Datum, Zahlen und Währung über Pipes mitLOCALE_IDundregisterLocaleData. - Es passt zu SSR/Prerender (eine Variante pro Locale, per URL gewählt), und mit Hydration ergänzt du
withI18nSupport(). Fang am ersten Tag an: i18n nachträglich einzubauen heißt, jeden hartcodierten String zu jagen.
Das fragen sie wohl als Nächstes
Wie organisierst du den Ablauf mit professionellen Übersetzern und hältst die XLIFF-Dateien über Releases hinweg synchron?
Warnsignal
Hartcodierte Formate wie
'dd/MM/yyyy'odertoFixed(2) + ' lei', String-Konkatenation für Sätze und keine eigenen Message-IDs.- Die Sprache mit dem eingebauten i18n zu wechseln bedeutet einen Reload, auch mit
S-19Deine Firma baut ein eigenes Design System in Angular. Wie machst du Barrierefreiheit zu einer Eigenschaft des Systems statt jedes einzelnen Screens? Barrierefreiheit
Das sagst du zuerst
Baue die interaktiven Patterns auf Angular Aria (
@angular/aria, seit v21): Headless-Direktiven wie Tabs, Menu, Combobox, Listbox, Tree oder Grid, die korrekte ARIA-Rollen, Tastaturnavigation und Fokus-Management ohne jegliche Styles liefern, während das Team das CSS besitzt. So bekommt jedes Produktteam Barrierefreiheit standardmäßig, statt Tabs und Dropdowns von Hand neu zu implementieren.Dann geh tiefer
- Wahl der Schicht: Aria für ein eigenes Design System, CDK für Low-Level-Primitive (Overlay, Drag & Drop, A11y-Utilities), Material, wenn du schnell eine fertige UI willst.
- Style nach dem ARIA-Zustand, den die Direktiven setzen (
[aria-selected='true']), damit visueller und zugänglicher Zustand nicht auseinanderlaufen können. - Kompromisse an anderer Stelle: Virtual Scroll entfernt Zeilen außerhalb des Sichtbereichs aus dem DOM (Strg+F und Screenreader sehen sie nicht); bevorzuge Paginierung, wo volle Barrierefreiheit zählt.
- Prozess: Tastatur- und Screenreader-Prüfungen in der Definition of Done, automatisierte Prüfungen in der CI und ein A11y-Review als Teil des Vertrags des Design Systems.
Das fragen sie wohl als Nächstes
Ein Team nutzt bereits Angular Material. Würdest du es auf Aria migrieren?
Warnsignal
„Wir ergänzen
aria-*-Attribute am Ende, vor dem Release“ — Barrierefreiheit wird als abschließende Checkliste behandelt statt als Eigenschaft der Komponenten.S-20Du leitest das Upgrade einer großen Anwendung von Angular 21 auf 22. Welche Breaking Changes bereiten dir Sorgen, und wie rollst du es aus? Architektur
Das sagst du zuerst
Eine Major-Version nach der anderen mit
ng update @angular/core@22 @angular/cli@22, und die Migrationen laufen lassen — sie ergänzen z. B.ChangeDetectionStrategy.Eager, wo Komponenten keine Strategie hatten, sodass das Verhalten identisch bleibt, obwohl OnPush zum Standard wird. Das eigentliche Risiko liegt in den Runtime-Änderungen, die Migrationen nicht vollständig erkennen: das Fetch-Backend, die Vererbung von Router-Params und die Template-Semantik.Dann geh tiefer
- HTTP: Fetch ist das Standard-Backend und
withFetch()ist deprecated; Upload-Fortschritt brauchtwithXhr(), und ausreportProgresswerdenreportUploadProgress/reportDownloadProgress. - Router und Templates:
paramsInheritanceStrategy: 'always'(Child Routes erben Params und Data von allen Parents),?.liefertundefinedwie in TypeScript, ein strengerer Compiler (doppelt gebundene Inputs,data-*bindet keine Inputs mehr),CanMatchFnbrauchtcurrentSnapshot. - Plattform und Entfernungen: TypeScript 6 und kein Node 20;
ComponentFactoryResolver,ComponentFactoryundprovideRoutessind weg; für SSR konfigurierst duallowedHostsund entfernstwithIncrementalHydration(). - Rollout: ein Upgrade-Branch, der in der CI grün gehalten wird, zuerst Bibliotheken und Node/TS anheben, E2E auf kritischen Abläufen, danach ein Folgeplan,
EagerFeature für Feature zu entfernen, statt es als Dauerzustand zu behandeln.
Das fragen sie wohl als Nächstes
Nach dem Upgrade erhält eine verschachtelte Route plötzlich einen Param, den sie vorher nicht bekam. Was hat sich geändert, und wie prüfst du, ob es relevant ist?
Warnsignal
package.json-Versionen von Hand ändern undng updateüberspringen oder mehrere Major-Versionen in einem Schritt springen, ohne das Changelog zu lesen.- HTTP: Fetch ist das Standard-Backend und
So antwortest du gut
- Zuerst ein klarer Satz, Details nur auf Nachfrage.
- Ein konkretes Beispiel aus einem Projekt: „Wir hatten X, wir haben Y gewählt, weil Z“.
- Den Trade-off nennen und den Fall, in dem du anders entscheiden würdest.
- „Ich weiß es nicht, aber so würde ich es herausfinden“, wenn du es wirklich nicht weißt.
- Definitionen aufsagen, ohne zu sagen, wann du sie einsetzen würdest.
- Veraltete Antworten als aktuell darstellen (überall NgModules, Zone.js als Selbstverständlichkeit).
- „Kommt drauf an“, ohne zu sagen, worauf.
- Eine API erfinden, um Schweigen zu vermeiden.