Angular Interview Prep

Angular Interview Questions and Answers (2026)

Angular interview questions usually test whether you understand the framework's underlying systems — dependency injection hierarchy, change detection, and RxJS — rather than just template syntax. Here are the ones that come up most.

  1. What's the practical difference between components, directives, and services?

    A component is a directive with a template — it controls a piece of UI and its own view. A directive (attribute or structural) adds behavior or modifies the DOM without owning a template of its own — e.g., ngClass (attribute) or *ngFor (structural, which restructures the DOM by adding/removing elements). A service is a plain injectable class with no view at all, used for logic that should be shared across components — HTTP calls, state, business logic — following Angular's separation of concerns.

    @Component({ selector: 'app-user', template: '<p>{{name}}</p>' })
    class UserComponent {}
    
    @Directive({ selector: '[appHighlight]' })
    class HighlightDirective {}
    
    @Injectable({ providedIn: 'root' })
    class UserService {}

    The mental model: components render, directives behave, services provide/compute.

  2. How does Angular's dependency injection hierarchy work?

    Angular DI is hierarchical, mirroring the component tree plus module-level injectors. When a component requests a dependency, Angular looks for a provider starting at that component's own injector, then walks up through parent components, then to the module injector, and finally the root injector (providedIn: 'root' registers a true app-wide singleton). Providing the same token at a lower level (e.g., in a specific component's providers array) shadows the parent's provider for that subtree only, which is how you get scoped instances — e.g., a fresh service instance per route or per component instance instead of the app-wide singleton.

    @Component({
      selector: 'app-cart',
      providers: [CartService], // new instance per CartComponent
    })
    class CartComponent {
      constructor(private cart: CartService) {}
    }

    This hierarchy is what enables patterns like feature-scoped state without polluting the global singleton.

  3. What are RxJS Observables and how do they differ from Promises?

    An Observable represents a stream of values over time that you subscribe to; unlike a Promise, it can emit zero, one, or many values, is lazy (nothing happens until .subscribe() is called), and is cancellable via unsubscription. Angular uses Observables pervasively — HttpClient responses, reactive forms valueChanges, router events — because many of these are naturally streams (a form value changing repeatedly) rather than single resolved values.

    this.http.get<User[]>('/api/users').subscribe({
      next: users => this.users = users,
      error: err => console.error(err),
    });

    Operators (map, filter, switchMap, debounceTime) let you compose transformations declaratively. A common bug source is forgetting to unsubscribe from long-lived observables, causing memory leaks — mitigated with the async pipe (auto-unsubscribes), takeUntil, or Angular's newer takeUntilDestroyed().

  4. Explain change detection — default vs OnPush.

    Angular's default change detection strategy checks every component in the tree on every change-detection cycle (triggered by events, timers, HTTP responses via Zone.js patching async APIs), comparing bound values to detect what needs re-rendering. This is simple but can get expensive in large trees. ChangeDetectionStrategy.OnPush tells a component to skip checking unless: (1) an @Input() reference changes, (2) an event originates from within the component, (3) an observable bound via the async pipe emits, or (4) change detection is manually triggered (markForCheck).

    @Component({
      selector: 'app-item',
      changeDetection: ChangeDetectionStrategy.OnPush,
    })
    class ItemComponent {
      @Input() item!: Item;
    }

    OnPush requires treating inputs as immutable — mutating an object in place won't change its reference, so Angular won't detect the change; you must pass a new object/array.

  5. What are the main lifecycle hooks and when does each run?

    In order: ngOnChanges (before ngOnInit, and on every subsequent change to an @Input()), ngOnInit (once, after the first ngOnChanges, ideal for initialization logic and initial API calls), ngDoCheck (every change detection run — for custom change detection logic), ngAfterContentInit/ngAfterContentChecked (after projected content via <ng-content> is initialized/checked), ngAfterViewInit/ngAfterViewChecked (after the component's own view and child views are initialized/checked — needed before safely accessing @ViewChild refs), and ngOnDestroy (cleanup — unsubscribe observables, clear timers, detach listeners).

    ngOnInit() { this.sub = this.svc.data$.subscribe(d => this.data = d); }
    ngOnDestroy() { this.sub.unsubscribe(); }

    ngOnDestroy is critical for preventing memory leaks in long-lived apps with routed components that mount/unmount repeatedly.

  6. How does two-way binding with `ngModel` actually work?

    [(ngModel)] is Angular's "banana in a box" syntax — sugar combining property binding [ngModel] (data flows into the input) with event binding (ngModelChange) (data flows out on change), which desugars to [ngModel]="value" (ngModelChange)="value = $event". It requires FormsModule and is part of Angular's template-driven forms approach, relying on directives that hook into native input events and push updates back to the bound model.

    <input [(ngModel)]="username" name="username">
    <!-- equivalent to -->
    <input [ngModel]="username" (ngModelChange)="username = $event">

    You can apply the same banana-in-a-box syntax to your own component's custom @Input()/@Output() pair as long as the output is named <input>Change, enabling two-way binding on custom components, not just native form elements.

  7. NgModules vs standalone components — what changed and why?

    Historically every component, directive, and pipe had to be declared in an NgModule, which bundled related pieces together and controlled what was exported/imported across feature modules — powerful but verbose, with a lot of boilerplate for small features and a steep learning curve around module boundaries. Standalone components (default since Angular 17+) let a component declare its own dependencies directly via an imports array, eliminating the need for a wrapping NgModule entirely for most apps.

    @Component({
      selector: 'app-widget',
      standalone: true,
      imports: [CommonModule, RouterModule],
      template: `...`,
    })
    class WidgetComponent {}

    This simplifies bootstrapping (bootstrapApplication instead of platformBrowserDynamic().bootstrapModule), improves tree-shaking, and reduces the mental overhead of the module system, though NgModules are still supported for gradual migration.

  8. What are route guards and what problem do they solve?

    Route guards are functions (or historically, injectable classes with methods like canActivate) that Angular's router calls before navigating to, leaving, or loading a route, letting you allow, block, or redirect navigation based on conditions like authentication or unsaved changes. Common guard types: canActivate (can this route be entered), canDeactivate (can we leave — e.g., "you have unsaved changes"), canMatch (should this route even be matched, useful for feature-flagged lazy routes), and resolve (pre-fetch data before activating).

    export const authGuard: CanActivateFn = () => {
      const auth = inject(AuthService);
      const router = inject(Router);
      return auth.isLoggedIn() ? true : router.parseUrl('/login');
    };

    Modern Angular favors these functional guards over class-based ones for less boilerplate and easier inject()-based DI.

  9. What are HttpClient interceptors and what are they typically used for?

    An interceptor sits in the middle of every outgoing HttpClient request and incoming response, letting you transform requests/responses or handle cross-cutting concerns in one place instead of repeating logic per call site. Common uses: attaching auth tokens to every request header, global error handling (redirect to login on 401), logging, retry logic, and showing/hiding a loading spinner.

    export const authInterceptor: HttpInterceptorFn = (req, next) => {
      const token = inject(AuthService).token;
      const cloned = req.clone({
        setHeaders: { Authorization: `Bearer ${token}` },
      });
      return next(cloned);
    };

    Requests are immutable, so interceptors clone the request to modify it. Multiple interceptors chain in registration order for the request, and reverse order for the response, forming a pipeline similar to middleware in Express.

  10. Template-driven vs reactive forms — how do they differ and when would you choose each?

    Template-driven forms (FormsModule, ngModel) define form structure and validation mostly in the template, with Angular implicitly building the form model behind the scenes — simple and quick for basic forms but harder to unit test and less predictable with complex, dynamic validation. Reactive forms (ReactiveFormsModule, FormGroup/FormControl) define the form model explicitly in the component class, giving synchronous access to values/validity, easier dynamic field manipulation, and straightforward unit testing without rendering a template.

    this.form = new FormGroup({
      email: new FormControl('', [Validators.required, Validators.email]),
    });

    Reactive forms are generally preferred for anything beyond a trivial form — dynamic validators, cross-field validation, or complex conditional fields — because the form's state lives in testable, explicit code rather than being inferred from the DOM.

  11. What's the difference between pure and impure pipes?

    A pure pipe (the default) only re-executes when Angular detects a change in the pipe's input reference (primitive change or object/array identity change) — cheap, since it avoids recomputing on every change detection cycle. An impure pipe (pure: false) re-executes on *every* change detection cycle regardless of whether the input actually changed, which is necessary for pipes that need to react to mutations inside the same array/object reference, but is expensive if overused.

    @Pipe({ name: 'filterActive', pure: false })
    class FilterActivePipe implements PipeTransform {
      transform(items: Item[]) { return items.filter(i => i.active); }
    }

    Built-in AsyncPipe is a notable impure pipe because it must check for new emissions on every cycle. Generally, prefer pure pipes and immutable data patterns over impure pipes for performance.

  12. How does lazy loading modules/routes work and why does it matter?

    Lazy loading defers downloading a feature's JS bundle until the user actually navigates to a route that needs it, rather than including everything in the initial bundle. This is configured in the router by pointing a route to a dynamic import() instead of eagerly importing the module or component, which most bundlers (via code-splitting) turn into a separate chunk fetched on demand.

    const routes: Routes = [
      {
        path: 'admin',
        loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent),
      },
    ];

    It matters for initial load performance — users only pay the download/parse cost for code they actually use — which is especially significant in large enterprise apps with many rarely-visited feature areas (admin panels, settings) that shouldn't bloat the app's critical initial bundle.

Practicing for a real interview? Get live AI help with your answers, in the browser, free to start.

Start a free session