Detailed answers, visual diagrams & interview tips — everything you need to work confidently in Angular.
The vocabulary every Angular developer must know — the framework's architecture, its building blocks, and the conventions that shape every app you'll ever build.
Angular is a TypeScript-based, component-driven front-end framework maintained by Google for building scalable single-page applications. It ships with a CLI, a reactive forms library, a router, HTTP client, and a testing harness — a complete platform, not a library.
AngularJS (1.x) is the original 2010 framework. It was written in JavaScript, used two-way binding by digest cycle, centered on controllers and $scope, and had no CLI or ahead-of-time compiler. Angular 2+ was a complete rewrite in 2016 with a totally new model.
The two are not compatible. Angular releases a new major version roughly every 6 months; there is no upgrade path from AngularJS short of rewriting the app.
An Angular app is a tree of components, wired together by services that the dependency injection system hands out. Templates, directives, pipes, and the router glue them together.
Layered on top: the Router maps URLs to components, the HttpClient fetches data, and the Forms library handles user input. Every app also has an entry point — bootstrapApplication() in modern Angular, or platformBrowserDynamic().bootstrapModule() in the legacy NgModule world.
A component is a TypeScript class decorated with @Component that controls a patch of the DOM. Each component has three inseparable parts: a template (HTML), a class (logic and state), and styles (CSS/SCSS, scoped by default).
Components form a tree: the root component (e.g., AppComponent) contains children, which contain grandchildren, and so on. They communicate downward via inputs, upward via outputs, and sideways via shared services.
Every component's selector becomes an HTML tag you can place in any parent template — that's how you compose a UI.
Angular calls specific methods on your component at well-defined moments. Implementing the matching interface (OnInit, OnDestroy, etc.) lets you hook in.
| Hook | Fires when | Typical use |
|---|---|---|
| ngOnChanges | Any @Input changes (including first) | React to parent-provided data |
| ngOnInit | Once, after first ngOnChanges | Initial HTTP calls, form setup |
| ngAfterViewInit | After view + children rendered | Measure DOM, init 3rd-party libs |
| ngDoCheck | Every change-detection run | Custom dirty-checking (rare) |
| ngOnDestroy | Before component is removed | Unsubscribe, clear timers |
In modern Angular you'll also see afterNextRender and afterRender — newer primitives for DOM work that play nicely with SSR.
Historically, every Angular app had one or more @NgModule classes. An NgModule was a container that declared its components, imported dependencies, and provided services — think of it as a manifest that told Angular what belongs together.
In Angular 14, standalone components arrived, and by Angular 17 they became the default. A standalone component imports what it needs directly, with no wrapping module required.
Directives and pipes can also be standalone. In Angular 19+ components are standalone by default — you don't even need to write standalone: true anymore.
The Angular CLI (ng) is the official command-line tool. It scaffolds projects, generates files, runs a dev server, bundles for production, and runs tests — all on top of esbuild (or Webpack, historically).
| Command | What it does |
|---|---|
| ng new my-app | Create a new Angular workspace with everything wired up. |
| ng serve | Dev server at localhost:4200 with hot reload. |
| ng build | Production bundle into dist/. |
| ng generate component|service|pipe | Scaffold files (shorthand: ng g c name). |
| ng test | Run unit tests (Karma/Jasmine or Vitest). |
| ng update | Bump Angular versions with automated migrations (schematics). |
| ng lint | Run ESLint against the workspace. |
| ng add @angular/material | Install & configure a library in one step. |
The killer feature is schematics: ng update not only upgrades dependencies, it also rewrites your code to match the new API — automating migrations that would otherwise take days.
TypeScript is a statically-typed superset of JavaScript. Angular was built in TypeScript and leans on it for almost everything — from templates to dependency injection.
In strictTemplates mode, Angular even checks your HTML: if you bind user.firstname but the model has firstName, the build fails. That's not something you get in a JavaScript framework.
The building-block mechanics: bindings that keep data and DOM in sync, decorators for communication, directives that reshape the DOM, and the new control-flow syntax.
Data binding is the syntax that keeps your component class and its template in sync. Angular has four kinds, organized by direction.
| Type | Syntax | Direction | Example |
|---|---|---|---|
| Interpolation | {{ value }} | Class → DOM text | <h1>{{ title }}</h1> |
| Property binding | [prop]="value" | Class → DOM property | <img [src]="url"> |
| Event binding | (event)="handler()" | DOM → Class | <button (click)="save()"> |
| Two-way | [(ngModel)]="x" | Both | <input [(ngModel)]="name"> |
Two-way binding only works when the directive exposes an @Output named xChange to match the @Input named x. That's the contract — Angular sugars the rest.
@Input marks a class property as "data that flows in from the parent". @Output marks an EventEmitter that fires events out to the parent. Together they implement Angular's unidirectional data-flow model.
In Angular 17.1+, the newer input() and output() signal-based APIs replace the decorators. input() returns a readable signal; input.required() makes the input mandatory; model() is the new two-way binding primitive.
Modern style (v17.1+): user = input.required<User>(); — read as user(), reactive by construction, plays nicely with OnPush and zoneless.
A directive is a class with @Directive that attaches behavior to an element. Angular has three flavors; the most interesting distinction is structural vs attribute.
The leading * in a structural directive is sugar. *ngIf="show" desugars to an <ng-template> wrapper, and Angular stamps it out only when the condition is true. That's why the element literally doesn't exist in the DOM when show is false.
The third flavor is the component itself — a special directive with a template. All three share the same DI and lifecycle.
*ngIf stamps a template into the DOM only when the expression is truthy, and tears it down when it turns falsy. *ngFor instantiates the template once per item of an iterable. *ngSwitch picks one branch from several like a switch statement.
By default, *ngFor identifies items by object identity. If you replace the whole array (common with immutable state), Angular destroys and rebuilds every node — expensive for long lists. trackBy returns a stable key per item so Angular can move nodes instead of re-creating them.
In Angular 17+ the @for block takes a mandatory track expression — Angular forces you to think about identity from the start.
Angular 17 introduced built-in control flow as a first-class template language feature. No directives, no imports — it compiles directly and is faster and smaller than the old *ngIf equivalents.
Angular ships a migration schematic — ng generate @angular/core:control-flow converts a whole codebase automatically.
Content projection lets a component accept markup from its caller and slot it into its own template. It's Angular's version of React's children prop or Vue's <slot>.
Use it whenever you're building layout components: cards, modals, tabs, expandable panels. The outer component owns structure and chrome; the caller owns content. This is the foundation of reusable UI kits.
Multiple slots are possible via select — a CSS selector that routes specific children to specific <ng-content> spots.
A template reference variable is a named handle to a DOM element or directive, created with the # prefix. You can use it anywhere else in the same template.
@ViewChild gets a reference from within your own template — the component's view. @ContentChild gets one from content projected in via <ng-content>. Both are the class-level, programmatic version of #.
Angular 17.2 added the signal-based viewChild() and contentChild() functions. They return a reactive signal, making timing issues ("it was undefined") mostly disappear.
Angular's superpower. A small, hierarchical DI system lets components stay thin while business logic, HTTP, and state live in reusable, testable services.
A service is a plain TypeScript class marked @Injectable(). It's where the logic that isn't part of "what the screen looks like" lives — HTTP calls, shared state, auth, logging, caching, business rules.
Why bother? Three reasons:
Dependency Injection (DI) is the pattern where a class asks for its dependencies instead of creating them. Angular reads the type signature (or inject() call), looks up a registered provider, and hands back an instance.
Three parts to remember:
| Concept | What it is |
|---|---|
| Token | The key to look up — usually a class, sometimes an InjectionToken<T>. |
| Provider | The recipe — useClass, useValue, useFactory, or useExisting. |
| Injector | The registry. There's a tree of them matching the component tree. |
The result: your classes never say new HttpClient(). They ask for an HttpClient and Angular decides what to give them — the real one in production, a mock in tests.
When you write @Injectable({ providedIn: 'root' }), you're telling Angular where to register the service. The scope determines how many instances you get.
| Scope | Lifetime | Use for |
|---|---|---|
| 'root' | App-wide singleton | 99% of services: auth, HTTP wrappers, state |
| 'platform' | Shared across multiple apps on the page | Micro-frontends |
| 'any' | One per lazy-loaded module | Module-scoped caches |
| Component-level | One per component instance | Per-form state, per-widget models |
Angular has a tree of injectors that mirrors the component tree. When a class asks for a dependency, Angular starts at that component's injector and walks upward until it finds a provider or hits the root.
This means you can shadow a service at a lower level. Provide LoggerService on a specific component, and that component and its children get the local one — while the rest of the app keeps the global one.
Resolution modifiers tune this: @Self() only looks locally, @SkipSelf() skips the current injector, @Optional() returns null if missing, @Host() stops at the host component.
Historically, Angular DI went through the constructor: constructor(private http: HttpClient) {}. Since Angular 14 the function inject() is the modern alternative — you call it inside a class field, function, or factory to get a dependency.
inject() must be called in an injection context: during class construction, inside a runInInjectionContext() block, or from functional guards/resolvers/interceptors. Call it from a random method and you'll get an error.
A class can serve as its own DI token — Angular looks it up by identity. But sometimes you need to inject something that isn't a class: a string, a config object, a factory, or an abstract interface. That's what InjectionToken is for.
The class would work for an instance, but not for a primitive — TypeScript interfaces are erased at runtime, so you can't use them as keys either. InjectionToken<T> fills both gaps.
The one-liner: @Injectable({ providedIn: 'root' }). Angular creates one instance per application injector and hands the same one to everyone who asks.
Rule of thumb: put providedIn: 'root' on the service, and never list it in a providers array. If you do both, you'll get multiple instances and race conditions.
How Angular turns URLs into rendered views — lazy loading, guards, resolvers, and the primitives that make multi-screen apps possible without a full page reload.
The Router is a service that maps URLs to component trees. You declare a config of Routes; the router watches the URL, matches against the config, builds the matching tree, and renders it into <router-outlet>.
You bootstrap it with provideRouter(routes) in standalone apps, or RouterModule.forRoot(routes) in NgModule ones.
Guards are functions (modern) or classes (legacy) that the router invokes at specific phases of navigation. They return a boolean, a UrlTree, or an Observable/Promise of one — and they can veto, redirect, or enrich a navigation.
| Guard | Fires | Typical use |
|---|---|---|
| CanMatch | Before a route is even considered | Feature flags, role-based route variants |
| CanActivate | Before activating a route | Auth: "must be logged in" |
| CanActivateChild | Before any child activates | Nested auth blocks |
| CanDeactivate | Before leaving a route | "You have unsaved changes" |
| Resolve | After guards, before render | Pre-fetch data so the view never flashes empty |
Lazy loading means the code for a route is only downloaded when the user navigates to it. This keeps the initial bundle small and the first paint fast.
Angular also supports preloading strategies — PreloadAllModules loads lazy chunks in the background after the app boots, giving you the speed of eager loading with the tiny initial bundle of lazy.
Both trigger navigation but at different layers. routerLink is a template directive — declarative, ideal for links and buttons. Router.navigate() is a programmatic API — use it from code when navigation is triggered by logic, not a click.
The Router offers three channels, each with different semantics.
| Channel | Example | Shows in URL | Typical use |
|---|---|---|---|
| Path params | /users/:id | Yes | Identifies the resource |
| Query params | /users?page=2 | Yes | Filters, sort, pagination |
| State | (opaque, in history) | No | Transient data, e.g., form draft on redirect |
Modern Angular also exposes withComponentInputBinding() — when enabled, path/query params are bound directly to @Input()s on the routed component. No ActivatedRoute subscription needed.
A route can have children. When you nest routes, each parent component hosts a <router-outlet>, and the matching child renders inside it. This lets you build layouts with persistent chrome — think an admin shell with a sidebar, where only the right pane changes per route.
You can also have named outlets for modal or secondary content: <router-outlet name="aside"> combined with outlet: 'aside' in the route config.
Angular's async model. Once you understand Observables, Subjects, and the four flattening operators, the HTTP client, Router, and Forms all become one consistent story.
RxJS (Reactive Extensions for JavaScript) is a library of Observables — lazy, multi-value, cancellable async streams — plus a rich set of operators to transform them. Angular adopted it as its default async model.
Almost every async surface in Angular returns an Observable: HttpClient.get(), route params, form value changes, Router.events. That consistency is the payoff — you learn one model, and it applies everywhere.
| Model | Values | Lazy? | Cancel? |
|---|---|---|---|
| Callback | Push, 0–N | — | Manual |
| Promise | Pull, 0–1 | No (eager) | No |
| Observable | Push, 0–N | Yes | Yes (unsubscribe) |
Two properties matter most: Observables are lazy (nothing runs until you subscribe), and they're cancellable (unsubscribe aborts the work). Promises are neither — a Promise fires the moment you create it and can't be stopped.
All three are async primitives, but they solve different problems.
| Promise | Observable | Subject | |
|---|---|---|---|
| Values | One | Zero to many | Zero to many |
| Lazy | No — runs on creation | Yes — on subscribe | Active — you push to it |
| Multicast | Yes (resolved once) | Default: unicast | Yes — broadcasts to all subs |
| Cancellable | No | Yes — unsubscribe | Yes — unsubscribe |
| Use for | One-shot request you can't cancel | Streams, HTTP, events | Cross-component event bus, state |
A Subject is both an Observable and an Observer. You can subject.next(x) to push a value, and any subscriber receives it. This makes Subjects useful as event buses and as the core of simple state stores.
RxJS has 100+ operators but you'll use the same dozen 90% of the time.
| Category | Operator | What it does |
|---|---|---|
| Transform | map | Maps each value through a function. |
| scan | Reduce but emits each intermediate — state accumulator. | |
| Filter | filter | Drops values that don't match. |
| distinctUntilChanged | Skips repeats. | |
| debounceTime / throttleTime | Rate-limits emissions (typeahead!). | |
| Combine | combineLatest | Combine latest values from multiple streams. |
| forkJoin | Like Promise.all — waits for all to complete. | |
| Flatten | switchMap / mergeMap / concatMap | Map each value to another Observable and flatten. |
| Error | catchError | Handle failures; return a fallback stream. |
| retry(n) | Resubscribe on error up to n times. | |
| Utility | tap | Side effects (logging) without changing values. |
| takeUntilDestroyed | Unsubscribe when component is destroyed. |
All four take a value, map it to an inner Observable, and flatten the result. The difference is how they handle overlap — what to do when a new outer value arrives while the previous inner is still running.
| Operator | Overlap behavior | Classic use |
|---|---|---|
| switchMap | Cancels the previous inner, starts the new one | Typeahead search — only latest matters |
| mergeMap | Runs all in parallel, merges emissions | Independent parallel requests |
| concatMap | Queues — runs one at a time, in order | Ordered writes, "save one at a time" |
| exhaustMap | Ignores new outer values while inner runs | "Don't double-submit the login form" |
Mnemonic: switch-cancel, merge-parallel, concat-queue, exhaust-debounce.
All four are Subjects — multicasting Observables you can push values into. The difference is what a late subscriber sees.
| Type | Late subscriber sees | Typical use |
|---|---|---|
| Subject | Only future emissions | Pure event bus |
| BehaviorSubject | The current value immediately, then future | State store — always has "current" |
| ReplaySubject(n) | Last n values, then future | Chat history, cache of last N |
| AsyncSubject | Only the final value, on complete | Cache of a one-shot async result |
BehaviorSubject is by far the most common — it's the building block of lightweight state stores, since it always has a "current" value you can synchronously read.
Subscriptions keep the Observable chain alive. If a component is destroyed but its subscription isn't, both the chain and any referenced DOM/state stay in memory — a leak that grows with every navigation.
The async pipe (| async) subscribes to an Observable (or Promise), returns the latest value for rendering, and unsubscribes when the view is destroyed. It's the single highest-leverage template feature in Angular.
The pipe also composes well. Need to transform before rendering? Put operators in the pipe() on the Observable — the template stays declarative.
Angular has two form systems. Knowing when to pick each, and how reactive forms model complex UIs, is the difference between fighting the framework and letting it help.
Angular has two form systems. They do the same job but use opposite philosophies.
| Template-driven | Reactive | |
|---|---|---|
| Source of truth | HTML directives | TypeScript FormGroup |
| Unit testability | Needs DOM | Pure function |
| Dynamic fields | Awkward | Trivial — formArray.push() |
| Typed | No | Yes (Angular 14+) |
| Debugging | Inspect DOM | Inspect a plain object |
For any form with more than a few fields, conditional rules, or dynamic controls — use Reactive. Template-driven is fine for a login form or a search box.
These are the three building blocks of reactive forms. They compose into any form shape you need.
| Class | Represents | Example |
|---|---|---|
| FormControl | A single input's value + state | One text field |
| FormGroup | A named object of controls | A user form with name, email, age |
| FormArray | An indexed list of controls | A dynamic list of phone numbers |
All three extend AbstractControl, which gives them value, status, valueChanges, statusChanges, and validators.
A sync validator is a function (c: AbstractControl) => ValidationErrors | null. An async validator returns a Promise or Observable of the same.
Attach them when constructing the control:
Two rules: async validators only run after sync validators pass, and they should emit once and complete (use first()) — otherwise the control stays in PENDING forever.
Before v14, FormControl.value was typed as any. Every access was unsafe — a typo in a control name silently returned undefined. Typed forms fix this by inferring the shape of the form from its structure.
Nullable vs non-nullable: by default, calling control.reset() sets the value to null, so the value type is string | null. Pass nonNullable: true to make reset restore the initial value and keep the type clean.
Use FormBuilder.nonNullable for a whole form: fb.nonNullable.group({...}) — every control becomes non-nullable automatically.
A ControlValueAccessor (CVA) is the contract that lets your component participate in Angular forms as if it were an <input>. Implement the interface and register the component as a value accessor, and you get to use formControlName, ngModel, validation states — everything.
Now you can write <app-rating formControlName="score"> and it behaves like a native input. The parent form doesn't care how the UI is implemented — it's a sealed black box that reads and writes a value.
Each AbstractControl exposes a handful of boolean flags describing its state. They drive styling (ng-dirty classes) and logic (only submit if valid, etc.).
| Flag | True when | Opposite |
|---|---|---|
| pristine | User hasn't changed the value | dirty |
| dirty | User changed the value (even back to original) | pristine |
| untouched | User hasn't blurred the field | touched |
| touched | User blurred the field at least once | untouched |
| valid | All validators pass | invalid |
| pending | Async validation is running | — |
| disabled | Control is disabled (excluded from value/validation) | enabled |
UX rule: show a validation error only when the control is invalid AND touched. Showing errors on an untouched, never-edited field feels hostile.
Programmatic changes via setValue / patchValue do not make a control dirty unless you also call markAsDirty(). This is usually what you want — server-fetched values shouldn't look "changed".
How Angular decides when to repaint the DOM — Zone.js, OnPush, Signals, zoneless. The mechanics behind every "why isn't my view updating" bug and every slow-scroll complaint.
Change detection is the process of comparing component data to the DOM and updating what differs. Angular runs it in a depth-first walk of the component tree, starting at the root, checking each binding, and updating any that changed.
What triggers a tick? By default, anything asynchronous: a click, a setTimeout, an XHR response, a Promise resolving. Zone.js monkey-patches the browser's async APIs and notifies Angular every time one runs.
Each component's template compiles into a dirty-checking function. The comparison uses === for primitives and reference equality for objects. That's why mutating an array in place doesn't trigger updates under OnPush — the reference didn't change.
Zone.js is a library that patches every asynchronous browser API — setTimeout, fetch, event listeners, promises — so that Angular can be notified whenever one of them runs. A "zone" is an execution context that persists across async boundaries.
Historically Zone.js made Angular "magical" — you could mutate any property and the view would update. The cost: extra bundle weight (~30 KB), CPU overhead, and confusing stack traces. Angular 18 made Zone.js optional via the zoneless change detection path.
You can opt part of your app out of the zone with NgZone.runOutsideAngular() — useful for hot-path operations like mouse-move or animation frames that shouldn't trigger change detection.
OnPush changes the rule for when a component's change detector runs. Instead of "every tick", it runs only when:
With OnPush, Angular skips whole subtrees that haven't been marked dirty. On a big app this can halve or quarter CD cost. The tradeoff: your app has to be immutable — replace arrays/objects instead of mutating them.
Signals are a new reactive primitive introduced in Angular 16 (stable in 17). A signal holds a value; reading it tracks a dependency; writing to it automatically notifies anything that reads it.
| Primitive | Purpose |
|---|---|
| signal(value) | Writable reactive value — read with x(), set with x.set() / x.update(). |
| computed(fn) | Derived read-only signal — recomputes when inputs change, cached otherwise. |
| effect(fn) | Side-effect that runs when any read signal changes. Used for logging, DOM work. |
Using a signal in a template makes the component's binding glitch-free reactive — Angular knows exactly which signals a view depends on and only re-renders when those change, even without OnPush.
Signals are synchronous. For async work, keep using Observables and bridge with toSignal() / toObservable().
Zoneless change detection is an Angular 18+ mode where the app runs without Zone.js. Instead of being notified on every async task, Angular schedules change detection only when signals change, async pipes emit, or explicit markForCheck() calls happen.
To go zoneless successfully, you need: signals for state, async pipes for streams, and OnPush-style immutability in any remaining legacy code. The rewards: smaller bundle, faster startup, and crisper stack traces.
Performance wins stack on three layers: bundle size, render work, and network.
Measure first: ng build --stats-json + source-map-explorer for bundles; the Angular DevTools profiler for CD hot spots; Lighthouse for overall Web Vitals. Don't guess.
Topics that separate seniors from the rest — server rendering, hydration, the Ivy compiler, and the testing patterns every team expects from a production-ready developer.
Server-Side Rendering (SSR) means rendering the initial HTML of your app on a Node.js server, sending it to the browser, and letting Angular take over on the client. Angular Universal was the name of the old standalone library; since Angular 17 SSR is built into the CLI — ng new --ssr wires it up.
Benefits:
| Benefit | Why it matters |
|---|---|
| Faster First Contentful Paint | Users see content before JS has finished loading — huge on slow networks. |
| SEO | Crawlers get a fully rendered page — no more JS-rendering quirks. |
| Social previews | Open Graph and Twitter cards work because meta tags are in the served HTML. |
| Accessibility | Screen readers and no-JS clients see content from the start. |
Cost: you now run Node in production, and code that assumes window / document will break on the server. Guard with isPlatformBrowser().
Hydration is the process of the client-side Angular app attaching to the server-rendered DOM instead of throwing it away and rebuilding it. Before hydration, SSR apps would briefly show the server HTML, then replace it with a freshly-bootstrapped client tree — causing a visible flicker and wasted work.
Angular 17 added incremental hydration via @defer blocks: parts of the page can remain as static HTML until the user scrolls to them or interacts — then only those chunks hydrate. That cuts time-to-interactive dramatically on content-heavy pages.
Ivy is Angular's compiler and runtime, introduced as default in Angular 9 (2020). It replaced View Engine, the previous pipeline. The design goals were smaller bundles, faster compilation, and better debugging.
| View Engine (pre-v9) | Ivy (v9+) | |
|---|---|---|
| Compilation model | Global, one-shot | Per-component, incremental |
| Tree-shaking | Limited — unused code shipped | Aggressive — only used code ships |
| Bundle size | Larger baseline | ~30–40% smaller for small apps |
| Build speed | Slower, recompiles everything | Much faster incremental rebuilds |
| Errors | Cryptic framework stacks | Human-readable locations |
Ivy also enabled features that simply weren't possible before: locality (components compile without knowing about their NgModule), standalone components, and eventually fine-grained hydration. Everything modern Angular ships has Ivy underneath.
You don't "use" Ivy directly — it's the compiler. You benefit from it passively every time you ship.
Angular ships a testing harness — TestBed — plus a set of utilities for rendering components in a simulated environment. Paired with Jasmine/Karma (classic) or Jest/Vitest (modern), it covers the whole pyramid.
Two high-leverage tips: component harnesses (@angular/cdk/testing) give you a stable API instead of brittle querySelectors, and spectator (third-party) cuts the boilerplate of TestBed.configureTestingModule in half.
For end-to-end, Cypress and Playwright have mostly replaced Angular's old Protractor (deprecated in v12).
From fundamentals to zoneless change detection — everything an Angular engineer needs to walk into an interview confident and leave confident of the answer.