1. Bootstrapping an App with bootstrapApplication
Everything starts in main.ts. One function call tells Angular "here's my
root component, here's my global configuration — go."
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));
Compare that to the older, NgModule-based bootstrap, which you'll still see in production codebases for a while yet:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch((err) => console.error(err));
Same job, different shape: the NgModule version bootstraps a module that declares a root component; the standalone version bootstraps the component directly, with configuration passed as a plain object instead of module metadata.
2. Standalone Components, Directives & Pipes
A standalone component lists what it needs directly, in its own imports
array — no NgModule sitting between it and the pieces it depends on.
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { UserCard } from './user-card/user-card';
@Component({
selector: 'app-root',
standalone: true, // implicit and optional in current Angular — shown here for clarity
imports: [RouterOutlet, UserCard],
templateUrl: './app.html',
})
export class App {}
That imports array is the entire dependency list for this component's
template — if UserCard weren't in it, using <app-user-card>
in app.html would fail to compile. Directives and pipes work identically:
they're just added to the same array.
Because dependencies are declared per-component instead of per-module, a bundler can trace the exact graph of what's actually used and drop the rest — no more accidentally shipping a whole feature module for one component.
3. Providers & ApplicationConfig
App-wide services — the router, HttpClient, anything you want available
everywhere — are registered once, in app.config.ts, and passed into
bootstrapApplication.
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
],
};
Each provideX() function is a small factory that returns the providers a
feature needs — you'll meet provideRouter properly in Week 10 and
provideHttpClient in Week 14. For now, the pattern to remember is: app-wide
config lives in one array, in one file, instead of scattered across NgModule
imports.
4. Change Detection & the Render Cycle (Conceptual Overview)
After bootstrap, Angular needs to know when to re-check your components for
changes and update the DOM. Historically, it used a library called Zone.js to patch
async browser APIs (setTimeout, event listeners, Promises) so it would
automatically know "something happened, time to check for changes."
When a change-detection pass runs, Angular walks the component tree from the root downward, checking each component's template bindings against their current values and patching the DOM wherever they differ.
That's the whole picture for now — just enough to know the render cycle exists and runs top-down. Week 5 (Signals) introduces a more precise, dependency-tracked alternative, and Week 20 covers zoneless change detection in depth, including why newer Angular apps are moving away from Zone.js entirely.
5. The Component Tree
Every Angular UI is a tree of components, rooted at whatever you passed to
bootstrapApplication. Data generally flows down the tree (parent to
child, via inputs) and events flow up (child to parent, via outputs) — you'll
build this properly with input()/output() in Week 4, but the
shape is worth seeing now:
App (root)
├── HeaderComponent
├── DashboardComponent
│ ├── UserListComponent
│ │ └── UserCardComponent (repeated per user)
│ └── StatsPanelComponent
└── FooterComponent
Nothing here is Angular-specific magic — it's the same tree structure as the DOM itself, just with your components as the nodes instead of raw HTML tags. Understanding this tree is what makes state-management decisions in Week 17 intuitive: "how far up the tree does this piece of state need to live?" is the core question you'll keep asking.
6. Migrating a Legacy NgModule App (Conceptual Walkthrough)
You'll encounter NgModule-based codebases in the wild for years to come, so it's worth knowing how the pieces map across. The Angular CLI even ships a schematic that automates most of this:
ng generate @angular/core:standalone
The mapping it performs, conceptually:
@NgModule({ declarations: [...] })→ each declared component/directive/pipe getsstandalone: true(implicit today) and its ownimportsarray@NgModule({ imports: [SomeModule] })→ the individual standalone pieces that module used to export are imported directly wherever they're used@NgModule({ providers: [...] })→ moved intoApplicationConfig.providers(for app-wide) or a component's ownprovidersarray (for scoped)platformBrowserDynamic().bootstrapModule(AppModule)→bootstrapApplication(RootComponent, appConfig)
The schematic runs in three stages — converting declarations, then imports, then removing now-empty NgModules — and it's designed to leave your app working after every stage, so you can migrate incrementally instead of in one risky pass.
7. Hands-on Exercise
Bootstrap a standalone app from scratch — no CLI schematic
Instead of running ng new, hand-write every file involved in getting a three-level component tree on screen.
Requirements:
- Set up a minimal project by hand:
index.html,main.ts, and atsconfig.jsontargetingES2022— you can useng newfor the build tooling scaffolding only, then delete everything undersrc/appand start clean. - Write
main.tscallingbootstrapApplicationdirectly, with an inlineApplicationConfig(an emptyprovidersarray is fine). - Create three components by hand —
AppRoot,DashboardComponent, andStatCardComponent— each with its ownimportsarray, wired soAppRootrendersDashboardComponent, which renders threeStatCardComponentinstances. - Confirm it runs with
ng serveand renders exactly the tree from Section 5's diagram, three levels deep.
If a component's template isn't rendering, check the parent's imports array first — the single most common standalone-component mistake is using a selector in a template without importing the component that owns it.
8. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What two arguments does bootstrapApplication take, and what is each one for?
What two arguments does bootstrapApplication take, and what is each one for?
The root component to render (a class, not an instance), and an ApplicationConfig object holding app-wide providers. Angular creates the root component, attaches it to the DOM element matching its selector in index.html, and makes every provider in the config available throughout the app.
Q2
If <app-user-card> in a template doesn't render, what's the first thing to check?
If <app-user-card> in a template doesn't render, what's the first thing to check?
Whether UserCard is actually listed in the using component's imports array. Standalone components don't inherit availability from anywhere else — every component a template uses has to be imported by that exact component.
Q3
Where do app-wide services like the router or HttpClient get registered in a standalone app?
Where do app-wide services like the router or HttpClient get registered in a standalone app?
In the providers array of the ApplicationConfig passed to bootstrapApplication — typically via helper functions like provideRouter() and provideHttpClient(), which each return the exact set of providers that feature needs.
Q4
In the NgModule → standalone migration, what happens to a module's declarations array?
In the NgModule → standalone migration, what happens to a module's declarations array?
It disappears — there's no standalone equivalent of "declaring" something in a shared list. Each component, directive, or pipe that used to be declared becomes independently standalone, and anywhere it's used gets it added to that consumer's own imports array instead.