AngularAtlasAusg. 22.2

Kap. 01Start04 / 75

Angular-Interviewfragen

60 echte Interviewfragen für Junior, Mid und Senior, mit der passenden Antwort, der Nachfrage und den Warnsignalen. Dazu ein Probeinterview.

v21v22 ~0 Min. Lesezeit

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

  1. Antworte laut, bevor du die Antwort öffnest. Beim Lesen hast du das Gefühl, es zu wissen; im Interview musst du es aussprechen.
  2. 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.
  3. Markiere, was du kannst, mit „Kann ich“. Das wird in diesem Browser gespeichert, und das Mock-Interview stellt dir zuerst die übrigen Fragen.
  4. 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.

LiveInterview-Simulator

Antworte laut, als würde dir jemand zuhören. Decke dann die Musterantwort auf und sei ehrlich zu dir selbst.

Level
Fragen
Fragen, die du nicht als bekannt markiert hast, kommen zuerst.

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.

0 von 20 als bekannt markiert
  1. 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 in imports deklariert, ein NgModule ist also nicht nötig: Die App startet mit bootstrapApplication(App, appConfig).

    Dann geh tiefer

    • Du schreibst standalone: true nicht 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.ts als providers: provideRouter(routes), provideHttpClient(...) nur, wenn du ihn konfigurierst, und so weiter.
    • Eine alte Bibliothek, die nur ein NgModule exportiert, kann trotzdem direkt in die imports einer Komponente; Provider eines Moduls laufen über importProvidersFrom().
    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));

    Warnsignal

    Sagt, dass jede Komponente in einem AppModule deklariert werden muss, oder importiert CommonModule „zur Sicherheit“.

  2. 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" />

    Warnsignal

    Hält [value]="x" und value="x" für dasselbe (das zweite ist ein statischer String) oder schreibt [(checked)]="notifications()".

  3. J-03Wie schreibst du Bedingungen und Schleifen in einem Template und warum braucht @for ein track? Templates

    Das sagst du zuerst

    Mit dem eingebauten Control Flow: @if / @else, @for mit @empty und @switch / @case. track ist 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); $index nur 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. @for stellt $index, $first, $last, $even, $odd, $count bereit.
    • @if (user(); as u) liefert einen typisierten Alias ohne null. Seit v21.1 können sich mehrere @case einen 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>
    }

    Warnsignal

    Greift in neuem Code immer noch zu *ngIf/*ngFor oder kann nicht erklären, wofür track da ist.

  4. J-04Was ist @let und wann würdest du es verwenden? Templates

    Das sagst du zuerst

    @let deklariert 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 @for für einen Wert pro Element (z. B. ein Endpreis nach Rabatt).
    • Echte Logik gehört weiterhin in die Klasse, als computed(); @let dient der Lesbarkeit, nicht für Geschäftsregeln.

    Warnsignal

    Verwendet @let wie eine veränderbare Variable oder verlagert damit komplexe Geschäftslogik ins Template.

  5. 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 Migrationen ngclass-to-class und ngstyle-to-style mit.
    • Für das Host-Element der Komponente selbst nutzt du die host-Metadaten ('[class.is-loading]': 'loading()'), nicht @HostBinding.

    Warnsignal

    Baut Klassen-Strings von Hand in der Komponente zusammen oder manipuliert classList direkt am Element.

  6. 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 @Pipe und transform().
    • 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. Vermeide pure: false.

    Warnsignal

    Macht eine Filter-Pipe impure, damit es auf einer großen Liste „läuft“.

  7. 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 und output() für ausgehende Events. Der Parent bindet [value]="..." und (rate)="onRate($event)"; das Child ruft rate.emit(4) auf.

    Dann geh tiefer

    • Ein input() ist ein schreibgeschütztes Signal: Du liest es mit value() und leitest mit computed() daraus ab — ngOnChanges brauchst du nicht.
    • input.required<T>() macht ein fehlendes Binding zum Compile-Fehler; transform: numberAttribute / booleanAttribute wandeln max="10" oder ein bloßes readonly-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 nutzt model().
    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),
      );
    }

    Warnsignal

    Sagt, man brauche ngOnChanges, um auf einen Input zu reagieren, oder verwendet @Input/EventEmitter in neuem Code.

  8. J-08Was ist model() und wie unterscheidet es sich von input() + output()? Komponenten

    Das sagst du zuerst

    model() ist ein beschreibbares Signal, das als Input plus automatischer xChange-Output funktioniert, sodass der Parent Two-Way-Binding nutzen kann: [(checked)]="notifications". Ruft das Child set() oder update() 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 nutzt model() für alles.

  9. 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. Mit select sind mehrere Slots möglich, etwa ein Titel und Aktionen in einer Card.

    Dann geh tiefer

    • select nimmt einen Tag-, Attribut- oder Klassen-Selector ([card-title]); das <ng-content /> ohne select fängt alles Übrige auf, und du kannst darin Fallback-Inhalt platzieren.
    • Stolperfalle: Projizierter Inhalt wird vom Parent erzeugt, selbst wenn das Child ihn mit @if versteckt. 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.

    Warnsignal

    Übergibt HTML als String-Input und rendert es mit innerHTML, statt es zu projizieren.

  10. J-10Was sind signal() und computed()? Signals

    Das sagst du zuerst

    Ein signal() ist ein reaktiver Wert: Du liest ihn, indem du ihn aufrufst, und änderst ihn mit set() oder update(). Ein computed() 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; nutze update((list) => [...list, x]).
    • Faustregel: Alles, was sich aus anderem Zustand berechnen lässt, ist ein computed(), kein zweites, synchron gehaltenes Signal. Gib Zustand mit asReadonly() 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() }}

    Warnsignal

    Versucht set() auf einem computed() oder vergisst die Klammern und bindet die Signal-Funktion statt ihres Werts.

  11. J-11Was ist effect() und wann solltest du es nicht verwenden? Signals

    Das 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() (oder linkedSignal(), 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 onCleanup räumt Timer vor dem nächsten Lauf auf.

    Warnsignal

    Nutzt effect(), um fullName mit firstName und lastName synchron zu halten.

  12. J-12Was ist der Unterschied zwischen Konstruktor und ngOnInit? Wie räumst du auf, wenn eine Komponente zerstört wird? Komponenten

    Das 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. ngOnInit läuft einmal, nachdem die ersten Inputs gesetzt sind. Zum Aufräumen nutze ich inject(DestroyRef).onDestroy() oder takeUntilDestroyed().

    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 du ngOnInit selten: Ein computed() oder httpResource() liest den Input, wenn er bereit ist.
    • inject() funktioniert nicht in ngOnInit oder in einem Click-Handler — das liegt außerhalb des Injection-Kontexts (Fehler NG0203).
    • DestroyRef funktioniert auch in Services, Direktiven und wiederverwendbaren Funktionen, was ngOnDestroy nicht kann. Für DOM-Zugriff nach dem Rendern nutze afterNextRender().
    export class Clock {
      readonly now = signal(new Date());
    
      constructor() {
        const id = setInterval(() => this.now.set(new Date()), 1000);
        inject(DestroyRef).onDestroy(() => clearInterval(id));
      }
    }

    Warnsignal

    Sagt, der Konstruktor sei „nur für DI“, und packt die gesamte Logik in ngOnInit, oder räumt Intervalle und Subscriptions nie auf.

  13. 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 mit inject(CartService).

    Dann geh tiefer

    • @Service() entspricht @Injectable({ providedIn: 'root' }): wird bei der ersten Injection erzeugt und ist Tree-Shaking-fähig. Es erfordert inject() — Konstruktor-DI ist ein Compile-Fehler.
    • @Injectable bleibt gültig (bestehender Code, Bibliotheken für ältere Versionen). Legst du den Service in die providers einer Komponente, bekommst du eine Instanz pro Komponente statt einer pro App.
    • inject() funktioniert auch in funktionalen Guards, Resolvern und Interceptors und vermeidet lange super(...)-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]);
      }
    }

    Warnsignal

    Erzeugt den Service mit new CartService() oder glaubt, ein Service in providers sei weiterhin derselbe globale Singleton.

  14. J-14Wie richtest du Routing ein und navigierst zwischen Seiten? Routing

    Das sagst du zuerst

    Du definierst ein Routes-Array, registrierst es mit provideRouter(routes) und setzt <router-outlet /> dorthin, wo die Seiten gerendert werden. Navigiert wird mit routerLink in Templates und router.navigate() im Code; Seiten werden meist per loadComponent lazy geladen.

    Dann geh tiefer

    • loadComponent: () => import('./home-page') legt die Seite in einen eigenen Chunk, der nur beim Besuch der Route geladen wird; loadChildren macht dasselbe für ein ganzes Feature.
    • Die Reihenfolge zählt — der erste Treffer gewinnt —, daher steht der Wildcard '**' ganz am Ende. routerLinkActive fügt dem aktiven Link eine Klasse hinzu.
    • Nutze routerLink, nicht href: href lädt die gesamte Anwendung neu.

    Warnsignal

    Navigiert mit <a href> oder window.location oder setzt die Wildcard-Route an die erste Stelle.

  15. J-15Wie liest eine Seite einen Route-Parameter wie :id? Routing

    Das sagst du zuerst

    Mit withComponentInputBinding() in provideRouter kommen 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/1 zu /products/2 verwendet dieselbe Komponenteninstanz weiter; Code, der die ID nur einmal liest (z. B. ein Snapshot in ngOnInit), zeigt veraltete Daten.
    • Parameter sind in der URL Strings, daher liefert dir ein Transform wie numberAttribute eine 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());
    }

    Warnsignal

    Kennt nur route.snapshot.paramMap.get('id') und weiß nicht, dass die Komponente wiederverwendet werden kann.

  16. 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() und error() als Signals und bricht alte Requests ab; für Schreibzugriffe (POST, PUT) rufst du HttpClient auf.
    • 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.

    Warnsignal

    Ruft http.get() ohne Subscribe auf und wundert sich, dass nichts passiert, oder ruft fetch direkt aus Komponenten auf.

  17. 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 wie required(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 nur ControlValueAccessor unterstü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" />

    Warnsignal

    Validiert von Hand im Submit-Handler mit ifs oder sagt, Signal Forms seien in v22 noch nur experimentell.

  18. J-18Was macht @defer? Performance

    Das sagst du zuerst

    @defer lä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äßig on idle, oder on viewport, on interaction, when cond(). Es verkleinert das initiale Bundle, ohne die Routen anzufassen.

    Dann geh tiefer

    • @placeholder wird vor dem Trigger angezeigt (nötig für on viewport / interaction / hover), @loading wä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 hover lä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 />
    }

    Warnsignal

    Hält @defer für ein bloß verzögertes @if und erwähnt den separaten JavaScript-Chunk nicht.

  19. 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äßig OnPush.

    Dann geh tiefer

    • Ohne Zone.js aktualisiert ein setTimeout oder subscribe(), das ein einfaches Feld ändert, die UI nicht. Die Lösung ist ein Signal (oder toSignal() / 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.Eager ist das alte Verhalten „immer prüfen“ (Default ist deprecated); ng update fügt es dort ein, wo es nötig ist.

    Warnsignal

    Behauptet bei einer v21+-App, Zone.js erkenne jede Änderung automatisch, oder behebt fehlende Updates, indem überall detectChanges() verstreut wird.

  20. J-20Wie testest du eine Komponente in einem neuen Angular-Projekt? Testing

    Das sagst du zuerst

    Seit v21 führt ng test Vitest aus (Karma ist deprecated). Ich erzeuge die Komponente mit TestBed.createComponent(), setze Inputs, rufe await fixture.whenStable() auf, damit das Zoneless-Rendering abgeschlossen ist, und prüfe dann fixture.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 mit outputBinding() und vi.fn() überwacht.
    • Für HTTP nutzt du provideHttpClientTesting() und HttpTestingController statt eines echten Servers; Services und Signal-Stores lassen sich mit TestBed.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);
    });

    Warnsignal

    Hat nie einen Test geschrieben oder verlässt sich in einem Zoneless-Vitest-Projekt auf fakeAsync/tick von 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.

0 von 20 als bekannt markiert
  1. M-01Wann nutzt du computed() und wann effect()? Warum solltest du in einem Effect keine Signals setzen? Signals

    Das 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 verkapptes computed() oder linkedSignal().

    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 du onCleanup.
    • 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()));
    }

    Warnsignal

    „Ich nutze Effect, um zwei Signals synchron zu halten“ oder „computed und effect sind im Grunde dasselbe, effect läuft nur öfter“.

  2. M-02Welches Problem löst linkedSignal()? Nenne ein reales Beispiel. Signals

    Das sagst du zuerst

    Es ist ein beschreibbares Signal, das sich aus einer Quelle zurücksetzt: abgeleitet wie computed(), aber du kannst es auch mit set() 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 den previous-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 set Schreibzugriffe ab, zum Beispiel um eine Menge auf den verfügbaren Bestand zu begrenzen.

    Warnsignal

    Den Input in ngOnInit in ein Signal zu kopieren (es wird nie zurückgesetzt) oder per Effect zu synchronisieren.

  3. M-03Wie funktionieren resource / httpResource, und warum sind sie besser, als HTTP in einem Effect zu abonnieren? HTTP

    Das 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() und reload() bekommst du geschenkt, ohne Race Conditions.

    Dann geh tiefer

    • resource bekommt im Loader ein abortSignal; httpResource läuft über HttpClient, daher greifen Interceptors; rxResource nimmt einen Observable-Stream und verhält sich wie switchMap.
    • Gibt params undefined zurück, ist der Status idle: Der Loader läuft nicht. In v22 kannst du auch ResourceParamsStatus.IDLE / LOADING werfen und Resources mit ctx.chain() verketten.
    • hasValue() ist ein Type Guard; defaultValue entfernt undefined; parse validiert 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 mit update() (optimistisch) oder lädst sie mit reload() 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> }

    Warnsignal

    httpResource für einen POST beim Button-Klick zu verwenden oder nicht zu wissen, was mit einem laufenden Request passiert, wenn sich params ändern.

  4. 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 dessen value() an ein httpResource. Das Debounce filtert das Rauschen; die Resource bricht veraltete Requests ab, sodass immer die letzte Suche gewinnt.

    Dann geh tiefer

    • debounced() gibt eine Resource zurück: value() aktualisiert sich nach der letzten Änderung, und isLoading() 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.
    • wait kann 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), und validateHttp hat eine eigene debounce-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: [] },
      );
    }

    Warnsignal

    Ein setTimeout im Input-Handler mit manuellem subscribe() und ohne Abbruch des vorherigen Requests.

  5. 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 manuellen subscribe() setze ich takeUntilDestroyed() ein, das ihn an den DestroyRef der Komponente bindet.

    Dann geh tiefer

    • toSignal() ohne initialValue hat den Typ T | undefined; requireSync: true funktioniert nur, wenn die Quelle synchron emittiert (z. B. ein BehaviorSubject).
    • 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 explizit inject(DestroyRef). Für Cleanup ohne RxJS nutzt du DestroyRef.onDestroy().
    • RxJS behältst du für Websockets, das Kombinieren von Event-Streams, Retry/Backoff und Flattening-Strategien (switchMap, exhaustMap…). Signals kennen keine Zeit.

    Warnsignal

    „Signals ersetzen RxJS komplett“ — oder das Gegenteil: überall subscriben und Werte in Felder kopieren, ohne je abzumelden.

  6. M-06Erkläre switchMap vs. mergeMap vs. concatMap vs. exhaustMap, mit je einem Anwendungsfall. RxJS

    Das sagst du zuerst

    Sie unterscheiden sich darin, was passiert, wenn ein neuer Wert eintrifft, während das vorherige innere Observable noch läuft: switchMap bricht es ab (Suche), mergeMap führt parallel aus (Uploads), concatMap reiht der Reihe nach ein (geordnetes Speichern), exhaustMap ignoriert 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 mergeMap auf dem Bezahlen-Button, verlorene Speichervorgänge mit switchMap beim Autosave.
    • mergeMap akzeptiert ein Concurrency-Limit, z. B. mergeMap(upload, 3).
    • Setze catchError in die innere Pipe: Nach dem Flattening-Operator beendet der erste Fehler den äußeren Stream, und die Suche „stirbt“.
    • Mit Signals liefern dir httpResource und rxResource für Lesezugriffe bereits die Semantik von switchMap.
    // 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 clicks

    Warnsignal

    „Ich nutze immer switchMap“ oder verschachtelte subscribe()-Aufrufe statt eines Flattening-Operators.

  7. M-07Was löst in einer zoneless Angular-22-App die Change Detection aus, und warum erscheint ein in setTimeout aktualisiertes Feld womöglich nicht? Change Detection

    Das 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 in setTimeout oder subscribe() gesetzt wird, benachrichtigt niemanden — mach es zu einem Signal.

    Dann geh tiefer

    • Seit v22 sind Komponenten ohne changeDetection OnPush; das alte Verhalten ist ChangeDetectionStrategy.Eager (Default ist deprecated). ng update ergä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, der async-Pipe oder markForCheck(). 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));
      }
    }

    Warnsignal

    Überall detectChanges() aufzurufen oder zu glauben, OnPush heiße „die Komponente aktualisiert sich nur, wenn sich Inputs ändern“.

  8. M-08Wie unterscheiden sich viewChild() und contentChildren() von den alten Dekoratoren, und wann kannst du sie sicher lesen? Komponenten

    Das sagst du zuerst

    Signal-Queries liefern Signals, die Ergebnisse sind also reaktiv: Du leitest daraus mit computed() ab statt mit ngAfterViewInit und QueryList.changes. Im Konstruktor existiert die View noch nicht, DOM-Arbeit gehört also in afterNextRender(); viewChild.required() wirft einen Fehler, wenn du zu früh liest.

    Dann geh tiefer

    • viewChild sucht im eigenen Template der Komponente; contentChild sucht in dem, was der Parent projiziert hat (z. B. ein Tabs-Container, der seine Tab-Kinder findet).
    • contentChildren betrachtet standardmäßig nur direkte Kinder; für tiefere Treffer nutzt du descendants: true.
    • read bestimmt, was du bekommst: ElementRef, TemplateRef, ViewContainerRef, eine Direktive oder (v22.2) den Injector des 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.

    Warnsignal

    Eine View-Query im Konstruktor oder in ngOnInit zu lesen und sich zu wundern, dass sie undefined ist.

  9. M-09Content Projection mit ng-content vs. ein mit ngTemplateOutlet gerendertes ng-template: Wann wählst du was? Templates

    Das sagst du zuerst

    ng-content ist für statische Slots: Der Parent erzeugt den Inhalt einmal, egal ob das Kind ihn anzeigt. Ein ng-template ist 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-content in ein @if packst, blendest du das DOM aus, aber die projizierten Komponenten existieren bereits und ihr Lifecycle ist schon gelaufen.
    • Benannte Slots nutzen select (Tag, Attribut, Klasse); ngProjectAs projiziert mehrere Elemente ohne Wrapper in einen Slot.
    • Der Kontext reicht Daten zurück: $implicit landet in let-item. Ohne eine Direktive mit ngTemplateContextGuard sind die let--Variablen any.
    • 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>

    Warnsignal

    Zu glauben, ein @if um ng-content mache den projizierten Inhalt lazy.

  10. 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>): T wird aus rows inferiert, Spaltenschlüssel sind auf keyof T beschrä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 in shared/.
    • Zellen-Templates findet man mit contentChildren() an einer Direktive; ngTemplateContextGuard typisiert let-row als T statt als any.
    • Ü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.

    Warnsignal

    Jedes Mal einen weiteren booleschen Input hinzuzufügen, wenn eine Seite etwas anderes braucht.

  11. 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 → NgComponentOutlet mit inputs; volle Kontrolle (Outputs, Direktiven) → ViewContainerRef.createComponent() mit bindings (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 alte setInput(), das du bei jeder Änderung erneut aufrufen musstest, plus manuelle Output-Subscriptions.
    • ComponentFactoryResolver wurde in v22 entfernt: Du übergibst die Klasse direkt an createComponent.
    • Für Toasts und Dialoge außerhalb des Baums nutzt du die Standalone-Funktion createComponent() mit einem EnvironmentInjector, danach appRef.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)),
        ],
      });
    }

    Warnsignal

    ComponentFactoryResolver oder innerHTML mit Komponenten-Tags zu verwenden.

  12. 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()) und computed()-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 providers der Seite oder Route (stirbt mit ihr), globaler Zustand (User, Warenkorb, Theme) in einem Root-@Service().
    • Server-Daten kommen in ein httpResource im 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.

    Warnsignal

    Jeden Zustand in einen einzigen globalen Root-Service zu packen oder beschreibbare Signals offenzulegen, die jede Komponente direkt verändert.

  13. 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? DI

    Das 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; providers an einer Route eine Instanz pro Feature; providers an 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) entspricht providedIn: 'root', ist tree-shakable und erlaubt nur inject(). autoProvided: false zwingt dich, ihn in providers aufzulisten.
    • inject() schlägt Konstruktor-DI: keine super(…)-Ketten, es funktioniert in Guards, Interceptors und Factories und ermöglicht wiederverwendbare injectX()-Funktionen. Es braucht einen Injection-Kontext (sonst NG0203).
    • useClass, useValue, useFactory, useExisting und multi: true für Plugin-artige Arrays; optional, self, skipSelf, host verändern die Suche.
    • viewProviders sind für projizierten Inhalt unsichtbar; EnvironmentProviders (provideX()) gehören nur auf App- oder Route-Ebene, nicht in eine Komponente.

    Warnsignal

    Einen Root-Service „sicherheitshalber“ in den providers einer Komponente aufzulisten — das erzeugt eine zweite Instanz und schwer auffindbare Zustandsfehler.

  14. M-14Funktionale Guards und Resolver: Was ist der Unterschied zwischen canMatch und canActivate, und wann würdest du einen Resolver vermeiden? Routing

    Das sagst du zuerst

    Guards sind einfache Funktionen, die inject() nutzen. canMatch entscheidet, ob die Route für den Benutzer „existiert“ — wenn nicht, probiert der Router die nächste Route, und der Lazy-Chunk wird nicht geladen. canActivate blockiert den Zugang zu einer Route, die gematcht hat. Ein Resolver verzögert die Navigation, bis Daten da sind; für Listen und Dashboards bevorzuge ich httpResource in der Seite.

    Dann geh tiefer

    • Gib true/false, einen UrlTree oder einen RedirectCommand zurück (Redirect + Navigationsoptionen wie replaceUrl); seit v22.2 kannst du auch einen RedirectCommand werfen.
    • 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 als input()s an.
    • Guards sind UX, keine Sicherheit: Die API muss die Autorisierung ebenfalls durchsetzen. canDeactivate schützt ungespeicherte Änderungen.

    Warnsignal

    Klassenbasierte Guards mit Konstruktor-Injection als Standard oder einen Guard als einzige Zugriffskontrolle zu betrachten.

  15. M-15Schreibe einen funktionalen HTTP-Interceptor, der ein Auth-Token hinzufügt. Was sind die Stolperfallen? HTTP

    Das sagst du zuerst

    Ein HttpInterceptorFn ist eine Funktion (req, next), die next(req) zurückgibt — oder einen Klon des Requests, denn Requests sind unveränderlich. Er wird mit provideHttpClient(withInterceptors([…])) registriert und kann inject() 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 wie catchError, die später und außerhalb des Injection-Kontexts laufen.
    • Nutze HttpContextToken für Metadaten pro Request (z. B. die Ladeleiste überspringen) statt falscher Header. Auch httpResource und validateHttp laufen ü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]));

    Warnsignal

    req.headers direkt zu verändern oder das Token an jede URL zu schicken.

  16. 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 @boundary isoliert; alles Unbehandelte landet in einem eigenen globalen ErrorHandler für Logging und Telemetrie.

    Dann geh tiefer

    • @boundary (v22.2, Developer Preview) hat @error (let e = $error; let reset = $reset)-Blöcke, optional mit when-Bedingungen — der erste Treffer gewinnt.
    • @boundary fängt nur Render-Fehler ab, keine Fehler aus Event-Handlern oder asynchronem Code — die erreichen weiterhin den ErrorHandler.
    • provideBrowserGlobalErrorListeners() leitet window.onerror und unbehandelte Rejections an den ErrorHandler weiter.
    • 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 in errors() 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()" />
    }

    Warnsignal

    Ein try/catch mit console.log in jeder Komponente oder Fehler in einem Interceptor zu verschlucken, sodass sie sonst niemand sieht.

  17. 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 in FormControls und stellen Änderungen als Observables bereit.

    Dann geh tiefer

    • Keine interne Kopie: model.set() aktualisiert die Inputs, und Tippen aktualisiert das Modell — kein patchValue / valueChanges.
    • Feldübergreifende Regeln nutzen valueOf() in validate; bedingte nutzen when, hidden, disabled (versteckte und deaktivierte Felder werden nicht validiert); wiederverwendbare Regeln nutzen schema() + apply().
    • validateHttp läuft über HttpClient (Interceptors greifen also) und hat in v22 eine debounce-Option; validateAsync funktioniert mit jeder Resource. pending() ist true, solange sie läuft.
    • Server-Fehler aus submission.action können mit einem fieldTree zurückgegeben werden, damit sie beim richtigen Feld landen.
    • Die Migration kann schrittweise mit SignalFormControl und compatForm erfolgen; 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' }),
      });
    });

    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 pending ist.

  18. 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 ist value = model(); danach funktioniert [formField] darauf genau wie auf einem input. Für Reactive Forms ist der klassische Weg ControlValueAccessor, registriert mit NG_VALUE_ACCESSOR.

    Dann geh tiefer

    • Optionale Inputs wie disabled, invalid, touched werden vom Formular automatisch befüllt — deklariere nur, was du nutzt. In v22 meldet ein touch-Output, dass der Benutzer das Control berührt hat.
    • Boolesche Controls implementieren FormCheckboxControl mit checked statt value.
    • Ein CVA braucht vier Methoden (writeValue, registerOnChange, registerOnTouched, setDisabledState), und du musst onChange selbst aufrufen; vergisst du onTouched, 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.

    Warnsignal

    Ein FormControl als Input an die Kindkomponente zu übergeben und es dort zu verändern.

  19. M-19Wie funktioniert @defer, welche Trigger würdest du nutzen, und was sorgt dafür, dass ein Deferred-Block gar nicht wirklich lazy ist? Performance

    Das sagst du zuerst

    @defer verlagert die Komponenten des Blocks in einen eigenen Chunk, der bei einem Trigger geladen wird: on idle (Standard), viewport, interaction, hover, timer, immediate oder when eine Bedingung. prefetch lä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.
    • @placeholder wird vor dem Trigger angezeigt (und ist das beobachtete Element für viewport / interaction / hover ohne Ziel); @loading (after 100ms; minimum 500ms) vermeidet Flackern; @error behandelt einen fehlgeschlagenen Chunk-Download.
    • when feuert nur einmal; mehrere Trigger werden mit ODER kombiniert.
    • Neu: on viewport({ rootMargin: '200px' }) (v21), on idle(500) (v22). Mit SSR liefern hydrate 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> }

    Warnsignal

    Inhalte im sofort sichtbaren Bereich (Above the Fold) in @defer zu packen oder zu glauben, @defer verzögere einen HTTP-Request statt das Laden von Code.

  20. 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)] }), dann await fixture.whenStable(), da es keine Zone zum Flushen gibt. HTTP: provideHttpClient() + provideHttpClientTesting(), dann HttpTestingController.expectOne(url).flush(data) und verify() in afterEach.

    Dann geh tiefer

    • Vitest ist seit v21 der Standard-Runner (@angular/build:unit-test); Karma ist deprecated. Nutze vi.fn() für Output-Spies und vi.useFakeTimers() statt fakeAsync.
    • 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, auf computed()s prüfen. Effects laufen asynchron, also flushst du sie mit TestBed.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 });
    });

    Warnsignal

    In Unit-Tests das echte Backend anzusprechen oder wiederholt detectChanges() aufzurufen und auf private Felder zu prüfen.

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.

0 von 20 als bekannt markiert
  1. 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. Ein computed sieht 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). Ein computed, 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]),
    });

    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.

  2. 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 setTimeout oder Promises, daher rendert Angular nur, wenn es Bescheid bekommt: Ein im Template gelesenes Signal ändert sich, ein Template-/Host-Listener feuert, die async-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 oder toSignal(); 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));
      }
    }

    Warnsignal

    detectChanges() überall verstreuen, bis der Bildschirm sich aktualisiert, oder glauben, Zoneless heiße „Angular prüft seltener, ist also fehleranfälliger“.

  3. 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 auf NgZone.onStable warten. 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: Eager entfernen, 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: fakeAsync braucht Zone.js, also wechselst du auf await 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.

    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.

  4. 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/resource und 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 patchState auf, rxMethod übernimmt asynchrone Abläufe (mit catchError im inneren Stream), und signalStoreFeature beseitigt kopierten Loading-/Error-Code. Die Bibliotheken können neben einem bestehenden NgRx Store koexistieren.

    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.

  5. 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-imports in einer einzelnen Anwendung oder Nx-Tags mit @nx/enforce-module-boundaries in 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'] },
      ],
    }],

    Warnsignal

    Ordner nach Typ (components/, services/) und ein shared als Sammelschublade, mit Grenzen, die „mündlich vereinbart“ sind.

  6. 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.ts oder 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 HttpErrorResponse zu 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 }]

    Warnsignal

    Komponenten, die HttpClient oder den konkreten Adapter direkt injizieren, DTOs, die in Templates fließen, oder vier Schichten für zwei Formulare „weil Clean Architecture“.

  7. 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 strictVersion sein, 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.

    Warnsignal

    „Wir brauchen Micro-Frontends für Skalierbarkeit und Performance“ oder strictVersion: false setzen, damit die Versionswarnungen verschwinden.

  8. 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 Outputs CustomEvents, 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, nicht change).
    • 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(), mehrere define()-Aufrufe), verwende einen Tag, der sich vom Selektor der Komponente unterscheidet, und verwende ein zerstörtes Element nicht wieder — erzeuge ein neues.

    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.

  9. 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.ts legt für jeden Pfad RenderMode.Prerender, Server oder Client fest. 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; mit PrerenderFallback.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_INIT statt eines Soft-404; das Caching gilt pro Route — public, s-maxage nur für identische Inhalte, private, no-store für personalisiertes HTML.
    • In v22 konfigurierst du allowedHosts (oder NG_ALLOWED_HOSTS): Ein unbekannter Host erhält 400 als SSRF-Schutz. Kann niemand einen Node-Server betreiben, nimm Prerender mit outputMode: 'static'.

    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.

  10. 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 @defer ist der endgültige Inhalt bereits auf dem Bildschirm, du kannst also auch oberhalb des Folds verzögern, ohne Layout Shift.
    • hydrate never ist 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 und withEventReplay() ist gegenüber dem Standard redundant; das Opt-out ist withNoIncrementalHydration().
    • 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 -->

    Warnsignal

    hydrate never auf etwas mit Listenern setzen oder @placeholder entfernen, „weil man ihn ohnehin nie sieht“.

  11. 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, ngOnInit oder effect() window, localStorage, Layout oder Observer anfasst — das läuft auch auf dem Server. Reine Browser-Arbeit gehört in afterNextRender / afterRenderEffect oder 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 @defer oder afterNextRender.
    • 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 wie Date.now(), die sich zwischen den Plattformen unterscheiden.
    • Zoneless-Server sehen dein eigenes Async nicht (fetch, SDKs, Timer): Nutze resource() oder PendingTasks, 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 oder useFactory.
    • Im Team: ein SSR-Build in der CI, eine Review-Checkliste, und ngSkipHydration nur als dokumentierter, temporärer Patch erlaubt.

    Warnsignal

    if (typeof window !== 'undefined') quer durch die Komponenten gestreut, oder effect() als „Browser-only-Hook“ behandeln.

  12. 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, @defer für schwere Inhalte unterhalb des Folds (nie das LCP-Element), injectAsync() für schwere, selten genutzte Services, NgOptimizedImage mit priority auf dem LCP-Bild, gezielte Imports, geprüft mit source-map-explorer.
    • Rendern: OnPush + Signals, track auf einer stabilen ID, Ableitungen in computed() 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: budgets in der angular.json, die den Build fehlschlagen lassen, eine Performance-Prüfung der Schlüsselseiten pro Release und jemand, der für die Zahlen verantwortlich ist.

    Warnsignal

    Optimieren ohne Messung oder „überall SSR und @defer“ — auch auf dem Hauptinhalt, was den LCP verschlechtert.

  13. 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 zu unsafe:. 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 — ngCspNonce am Root-Element oder das CSP_NONCE-Token; security.autoCsp berechnet Hashes zur Build-Zeit, deckt aber nur Skripte ab und funktioniert nicht mit SSR.
    • Führe CSP zuerst mit Content-Security-Policy-Report-Only ein, sonst brechen Lazy Chunks oder Inline-Styles in Produktion.
    • CSRF bei Session-Cookies: HttpClient sendet das Cookie XSRF-TOKEN als X-XSRF-TOKEN bei verändernden Same-Origin-Requests — aber der Server muss es setzen und prüfen. Jede Frontend-Prüfung wird auf dem Server wiederholt.

    Warnsignal

    „Angular sanitisiert alles, also sind wir sicher“, während die Codebasis eine Pipe mit bypassSecurityTrustHtml auf CMS-Inhalten enthält.

  14. 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), in finalize geleert), 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 provideAppInitializer einen 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;
    }

    Warnsignal

    JWT in localStorage ohne Gedanken an XSS oder CSP, ein separater Refresh pro 401 oder Berechtigungsprüfungen auf Basis des im Frontend dekodierten JWT.

  15. 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 globalen ErrorHandler, der in die Telemetrie loggt.

    Dann geh tiefer

    • provideBrowserGlobalErrorListeners() leitet window.onerror und unbehandelte Rejections an den ErrorHandler weiter; halte den Handler einfach und sicher — wirft er selbst, verlierst du jeden Fehler.
    • @boundary fängt nur Rendering-Fehler, keine aus Event-Handlern oder asynchronem Code, und ein gefangener Fehler wird trotzdem an den ErrorHandler gemeldet.
    • Der Interceptor wirft nach der Behandlung erneut (throwError(() => err)), damit Aufrufer weiterhin reagieren können; ein catchError(() => 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.

    Warnsignal

    Ein try/catch oder catchError in jeder Komponente, das Fehler verschluckt, und ein Toast für jeden davon, ohne zentrales Logging.

  16. S-16Wie wählst du zwischen switchMap, mergeMap, concatMap und exhaustMap, und welche Stolperfallen haben shareReplay und Backpressure? RxJS

    Das sagst du zuerst

    Die Wahl ist die Überlappungs-Policy, wenn ein neuer Wert ankommt, während der vorherige innere Request noch läuft: switchMap bricht ab (Typeahead), mergeMap läuft parallel (unabhängige Uploads), concatMap reiht ein (Autosave, geordnete Befehle), exhaustMap ignoriert (Submit, Login). Falsche Entscheidungen sind Race Conditions, die nur in einem langsamen Netz auftreten.

    Dann geh tiefer

    • switchMap für Schreibzugriffe ist gefährlich: Ein abgebrochenes Speichern kann den Server erreicht haben oder auch nicht. Schreibzugriffe nutzen concatMap oder exhaustMap.
    • catchError gehört in die innere Projektion; nach switchMap platziert, beendet der erste fehlgeschlagene Request den gesamten Stream, und die Suche stirbt stillschweigend.
    • Ein schlichtes shareReplay(1) hat refCount: false: Bei einer unendlichen Quelle (WebSocket, Interval) meldet es sich nie ab. Nimm { bufferSize: 1, refCount: true } — und wisse, dass der Cache nur erhalten bleibt, wenn die Quelle abgeschlossen ist.
    • RxJS ist push-basiert und hat kein eingebautes Backpressure, also wählst du eine Policy: verwerfen (switchMap, exhaustMap, throttleTime, auditTime, debounceTime), bündeln (bufferTime) oder die Parallelität begrenzen (mergeMap(fn, 3)). Eine unbegrenzte concatMap-Queue bei einer schnellen Quelle lässt den Speicher wachsen.
    readonly results$ = this.query$.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap((q) =>
        this.api.search(q).pipe(
          catchError((err) => {
            this.log.error(err);
            return of([]);        // inside: only this request fails, the stream lives on
          }),
        ),
      ),
      shareReplay({ bufferSize: 1, refCount: true }),
    );

    Warnsignal

    „Ich nehme immer mergeMap“ oder verschachtelte subscribe()-Aufrufe — damit gehen Abbruch, Reihenfolge und Fehlerbehandlung verloren.

  17. 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 HttpTestingController und 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 Schematic refactor-jasmine-vitest umziehen.
    • Zoneless-Tests: await fixture.whenStable() statt fakeAsync, Vitest Fake Timers für Debounce, TestBed.tick(), um Effects auszuführen; Inputs als Signals über inputBinding.
    • 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 affected plus Cache für die Geschwindigkeit, kein Cache für nicht deterministische E2E-Tests gegen ein echtes Backend, und Coverage als Signal, nicht als Ziel.

    Warnsignal

    „100 % Coverage bei komplett gemocktem Code“ — Tests, die prüfen, ob private Methoden aufgerufen wurden, bei jedem Refactoring brechen und echte Bugs übersehen.

  18. 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 mit i18n / $localize markiert sind, mit ng extract-i18n extrahiert, 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 hat one, few und other („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 mit LOCALE_ID und registerLocaleData.
    • 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.

    Warnsignal

    Hartcodierte Formate wie 'dd/MM/yyyy' oder toFixed(2) + ' lei', String-Konkatenation für Sätze und keine eigenen Message-IDs.

  19. 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.

    Warnsignal

    „Wir ergänzen aria-*-Attribute am Ende, vor dem Release“ — Barrierefreiheit wird als abschließende Checkliste behandelt statt als Eigenschaft der Komponenten.

  20. 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 braucht withXhr(), und aus reportProgress werden reportUploadProgress / reportDownloadProgress.
    • Router und Templates: paramsInheritanceStrategy: 'always' (Child Routes erben Params und Data von allen Parents), ?. liefert undefined wie in TypeScript, ein strengerer Compiler (doppelt gebundene Inputs, data-* bindet keine Inputs mehr), CanMatchFn braucht currentSnapshot.
    • Plattform und Entfernungen: TypeScript 6 und kein Node 20; ComponentFactoryResolver, ComponentFactory und provideRoutes sind weg; für SSR konfigurierst du allowedHosts und entfernst withIncrementalHydration().
    • 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, Eager Feature für Feature zu entfernen, statt es als Dauerzustand zu behandeln.

    Warnsignal

    package.json-Versionen von Hand ändern und ng update überspringen oder mehrere Major-Versionen in einem Schritt springen, ohne das Changelog zu lesen.

So antwortest du gut

Was überzeugt
  • 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.
Was Punkte kostet
  • 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.

Kap. 01 · 0 von 4 abgeschlossen

Lektion abgeschlossen?

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

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

  1. Was ist neu in Angular 21 & 22
  2. Deine erste App, Schritt für Schritt
  3. Schnelle Entscheidungen
  4. Angular-Interviewfragen
Deine ganze Karte ansehen

Alle 75 Seiten · tippen zum Filtern

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