In Angular, how does bootstrapApplication start a standalone app, and what belongs in the ApplicationConfig passed to it?
answer
- one call in main.ts
- root must be standalone
- a single providers array
- returns Promise<ApplicationRef>
basics
~10 sbootstrapApplication(App, appConfig) in main.ts renders a standalone root component and builds the root injector from ApplicationConfig, whose only field is a providers array of plain providers and provideX() functions. It returns a Promise<ApplicationRef>.
solid answer
~30 s`main.ts` calls `bootstrapApplication(App, appConfig)` from `@angular/platform-browser`. The root component must be standalone, which is the default since v19, so there is no `AppModule`. `ApplicationConfig` is an interface with one field, `providers`, holding `Provider` and `EnvironmentProviders` entries: plain recipes, feature functions such as `provideRouter(routes)` or `provideHttpClient()`, and `importProvidersFrom(...)` for NgModule-only libraries. Those providers land in the root environment injector, visible to the whole app. The call registers the browser infrastructure itself, so `BrowserModule` is not imported, and it returns a `Promise<ApplicationRef>` that rejects if startup throws, which is why the CLI appends `.catch()`.
code
ts · 10 linesimport { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
],
};go deeper
Recall the two files: main.ts calls bootstrapApplication with the root component and appConfig, and app.config.ts exports a providers array. Know that no AppModule is involved.
Explain what the providers array accepts, that feature functions return EnvironmentProviders, why BrowserModule is gone, and that the call returns a promise of ApplicationRef.
Show you can reason about startup failures: which errors reject the bootstrap promise, and why app-wide providers belong in the config rather than on a component.
Frame standalone bootstrap as the app's single composition root, and argue for keeping it a short, reviewable list of provideX calls rather than a dumping ground.
## What bootstrapping means in a standalone Angular app **Bootstrapping** is the step that turns a folder of TypeScript classes into a running application: Angular creates a platform, builds the application's root **environment injector**, instantiates the root component, attaches it to the matching element in `index.html`, and starts change detection. In a standalone app (the default for components, directives and pipes since v19) the whole step is one call in `main.ts`: ```ts import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { App } from './app/app'; bootstrapApplication(App, appConfig).catch((err) => console.error(err)); ``` There is no `AppModule`, no `declarations` array and no `bootstrap: [...]` field. The Angular CLI generates exactly this shape for a new project. ## The three arguments of `bootstrapApplication` `bootstrapApplication` is exported from `@angular/platform-browser` and takes up to three arguments: 1. **`rootComponent`** — the class to render first. It **must** be a standalone component. Because standalone is the default, a plain `@Component({...})` class with no `standalone` flag qualifies; a component that declares `standalone: false` and lives in an NgModule's `declarations` does not. 2. **`options`** — an optional `ApplicationConfig`, conventionally exported as `appConfig` from `app.config.ts`. 3. **`context`** — an optional `BootstrapContext`. The browser never passes it; server rendering does, so that each request gets its own platform (the server side is a separate subject). The call is `async` and returns a **`Promise<ApplicationRef>`**. The promise resolves once the root component is created and attached; it rejects if anything during startup throws — a provider factory, an initializer, or a root selector that matches no element. That is why the generated `main.ts` ends with `.catch(...)`. ## What goes into `ApplicationConfig` `ApplicationConfig` is a small interface from `@angular/core`. Its only field is: ```ts interface ApplicationConfig { providers: Array<Provider | EnvironmentProviders>; } ``` Everything an app needs application-wide is expressed as a provider in that one array: - **Plain providers** — a class, or a recipe such as `{ provide: API_URL, useValue: '/api' }`. - **`provideX()` feature functions** — `provideRouter(routes)`, `provideHttpClient(...)`, `provideClientHydration()`, `provideBrowserGlobalErrorListeners()`. They return `EnvironmentProviders`, a type that is only accepted by environment injectors. - **`importProvidersFrom(SomeModule)`** — the bridge for a library that still ships only an NgModule. The CLI's generated `app.config.ts` for a new v22 project with routing contains `provideBrowserGlobalErrorListeners()` and `provideRouter(routes)`. It no longer contains a zone provider, because zoneless change detection is the default since v21. Providers listed here are registered in the **root environment injector**, so they are visible to the root component and everything below it — every component, directive, pipe, service and lazily-loaded route. ## What `bootstrapApplication` adds on its own The call already registers the browser infrastructure that `BrowserModule` used to bring: the DOM renderer, the event manager and its key-event plugin, a default `ErrorHandler`, and the `DOCUMENT` and platform-id tokens. You do not import `BrowserModule` into a standalone app. Template building blocks such as `@if` and `@for` are built into the template syntax, and anything else a template uses is listed in that component's own `imports` array rather than in the app config. ## Why `appConfig` lives in its own file Nothing forces the config into a separate module — `bootstrapApplication(App, { providers: [...] })` works inline. The CLI splits it into `app.config.ts` for practical reasons: - **Reuse.** A server-rendered app bootstraps twice, once in the browser and once on the server. The server build imports the same `appConfig` and merges its own providers on top with `mergeApplicationConfig`, so shared providers are written once. - **Tests and tools.** A test or a script can import the config without executing the bootstrap side effect that `main.ts` performs. - **Review.** The file is the application's single **composition root**: one short list that says which framework features are switched on. When a reviewer wants to know whether the app uses hydration, a custom error listener or a module-only library, this is the one place to look. Keeping `main.ts` to a single call also means the entry point never grows logic of its own; configuration changes are made in the config, not in the startup script. ## Module bootstrap compared | Concern | NgModule app | Standalone app | |---|---|---| | Entry call | `platformBrowser().bootstrapModule(AppModule)` | `bootstrapApplication(App, appConfig)` | | Where app-wide providers live | `@NgModule({ providers })`, `forRoot()` imports | `appConfig.providers` | | Where template dependencies live | `declarations` / `imports` of the module | each component's own `imports` | | Root component chosen by | `bootstrap: [App]` | the first argument | ## Common mistakes - Putting `provideRouter` or `provideHttpClient` in a **component's** `providers` array — they return `EnvironmentProviders`, which a component injector rejects. - Believing `ApplicationConfig` has `imports` or `declarations` fields — it has only `providers`. - Forgetting that the returned value is a promise and swallowing bootstrap failures. - Passing a `standalone: false` component as the root.
- Why does the CLI-generated main.ts end with .catch((err) => console.error(err))?`bootstrapApplication` is async and returns a `Promise<ApplicationRef>`. If startup throws, for example a provider factory fails, an initializer rejects, or the root selector matches no element in `index.html`, the promise rejects. Without a `.catch`, that surfaces as an unhandled promise rejection instead of a logged bootstrap error.
- Do you still import BrowserModule or CommonModule anywhere in a standalone app?No. `bootstrapApplication` registers the browser providers `BrowserModule` used to supply, such as the DOM renderer, event manager and default `ErrorHandler`. Built-in control flow needs no import, and a component that uses a pipe or directive lists that class, not `CommonModule`, in its own `imports`.
saying these in an interview costs you the question
- bootstrapApplication still needs an AppModule with a bootstrap array
- ApplicationConfig has imports and declarations fields like an NgModule
- You must add BrowserModule to the providers for the app to render
- Any component, including one with standalone: false, can be the root
- bootstrapApplication returns the root ComponentRef synchronously