Skip to content
Angular ng modules 4 min read

Bootstrapping the Application

Every Angular application needs an entry point that mounts the root component into the DOM and wires up the providers the rest of the app depends on. In modern Angular (17/18/19) that entry point is bootstrapApplication — a standalone-first API that replaces the old platformBrowserDynamic().bootstrapModule(AppModule) flow. Understanding how main.ts and app.config.ts fit together is the foundation for everything else: routing, HTTP, hydration, and dependency injection all start here.

The entry point: main.ts

When the Angular CLI builds your project, it uses main.ts as the bootstrap file (configured in angular.json under architect.build.options.main). In a standalone app, main.ts is intentionally tiny. It imports the root component and an application configuration object, then hands both to bootstrapApplication.

// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

bootstrapApplication returns a Promise<ApplicationRef>. It creates a root injector, instantiates AppComponent, and attaches it to the host element declared in the component’s selector (typically <app-root></app-root> in index.html). The .catch handler surfaces any synchronous bootstrap failures — for example a missing provider — so they don’t get swallowed.

The root component passed to bootstrapApplication must be a standalone component (standalone: true, which is the default in Angular 19). Passing an @NgModule-declared component here throws at runtime.

Structuring providers in app.config.ts

Because there’s no root AppModule in a standalone app, application-wide providers live in an ApplicationConfig object instead. By convention this is exported from app.config.ts. Keeping it in its own file keeps main.ts clean and makes the configuration easy to reuse — the server-side rendering entry point, for example, can merge its own config on top of it.

// src/app/app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(withFetch()),
  ],
};

The providers array accepts the same provider syntax you’d use anywhere in Angular: provide* configuration functions, class providers, value providers, and factory providers. The provide* functions (like provideRouter and provideHttpClient) are the standalone equivalents of importing feature modules such as RouterModule.forRoot() or HttpClientModule.

Common application providers

Provider functionPurposePackage
provideRouter(routes)Configures the router with top-level routes@angular/router
provideHttpClient(withFetch())Enables HttpClient using the Fetch API@angular/common/http
provideAnimationsAsync()Lazy-loads the animations engine@angular/platform-browser/animations/async
provideClientHydration()Reuses server-rendered DOM during SSR@angular/platform-browser
provideZoneChangeDetection(opts)Tunes Zone.js change detection@angular/core

Adding your own providers

The config is also where you register your own injectable values — environment tokens, interceptors, error handlers, and the like. You can mix provide* helpers with classic provider objects, and functional interceptors register through the router/HTTP helpers.

// src/app/app.config.ts
import { ApplicationConfig, ErrorHandler, InjectionToken } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './core/auth.interceptor';
import { GlobalErrorHandler } from './core/global-error-handler';

export const API_URL = new InjectionToken<string>('API_URL');

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
    { provide: API_URL, useValue: 'https://api.example.com' },
    { provide: ErrorHandler, useClass: GlobalErrorHandler },
  ],
};

Any component or service can then read these values with inject():

import { Component, inject } from '@angular/core';
import { API_URL } from './app.config';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `<p>Calling {{ apiUrl }}</p>`,
})
export class AppComponent {
  readonly apiUrl = inject(API_URL);
}

What happens during bootstrap

When bootstrapApplication runs, Angular performs these steps in order, and you can observe the result in the browser console if a provider is misconfigured.

bootstrapApplication(AppComponent, appConfig)
  .then(() => console.log('App bootstrapped'))
  .catch((err) => console.error('Bootstrap failed:', err));

Output:

App bootstrapped

If, for instance, you use HttpClient without calling provideHttpClient, the bootstrap promise rejects:

Output:

Bootstrap failed: NullInjectorError: No provider for _HttpClient!

Avoid importProvidersFrom(...) unless a library only ships an NgModule. Prefer the dedicated provide* functions — they tree-shake better and are the long-term direction of the framework.

Best Practices

  • Keep main.ts minimal: import the root component and appConfig, call bootstrapApplication, and handle errors with .catch.
  • Centralize application-wide providers in app.config.ts so SSR and test setups can reuse or extend them.
  • Prefer provide* functions over importProvidersFrom to maximize tree-shaking and stay aligned with modern Angular.
  • Enable provideZoneChangeDetection({ eventCoalescing: true }) to reduce redundant change-detection cycles.
  • Register cross-cutting concerns (HTTP interceptors, global error handler) in the config rather than scattering them across components.
  • Use inject() to consume tokens defined in the config instead of constructor injection where it keeps code terser.
  • Split large configs by feature and compose them, but keep a single exported appConfig as the source of truth for bootstrap.
Last updated June 14, 2026
Was this helpful?