Angular Bootstrap and ApplicationConfig: The provideX() Ecosystem [2026]

Link copied
Angular Bootstrap and ApplicationConfig: The provideX() Ecosystem [2026]

Angular Bootstrap and ApplicationConfig: The provideX() Ecosystem [2026]

Every Angular application starts at the same place: one call to bootstrapApplication() in main.ts, given a root component and a configuration object. That single function call is the entire wire-up — there is no AppModule, no BrowserModule, no forRoot() ceremony. The trade-off Angular made in v15 and finished in v17 was to lift everything global about your app into one declarative ApplicationConfig object.

This lesson is the one you reach for whenever someone on your team asks "where do I register the HTTP client?" or "where does provideRouter actually go?" By the end you will know what bootstrapApplication() does, what ApplicationConfig is, and how the provideX() family of functions stitches every cross-cutting Angular feature into your app — router, HTTP, animations, hydration, zoneless change detection, your own custom services, and the rest.

This is lesson 1.6 of the Angular Tutorial, part of Module 1 Foundations. It assumes you have read lesson 1.2 and have a scaffolded project on disk.

What bootstrapApplication() does #

Open the src/main.ts file in any 2026 Angular project. The whole file looks like this:

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));

Three imports and one function call. That call does five things, in order:

  1. Creates a root injector seeded with the providers in your ApplicationConfig.
  2. Resolves the root component (App) — Angular finds its @Component metadata, parses the template, hydrates its imports.
  3. Replaces the <app-root> placeholder in index.html with the rendered component output.
  4. Starts change detection — either zoneless (the default in v21) or zone-based (legacy fallback).
  5. Returns a Promise that resolves once the application reference is alive. Errors during bootstrap reject that promise.

That is the entire boot sequence. The two interesting pieces are the root injector (which decides what every service in your app can see) and the change-detection strategy (which decides how the UI re-renders). Both are configured through ApplicationConfig.

The shape of ApplicationConfig #

ApplicationConfig is an interface from @angular/core. The implementation is one property:

export interface ApplicationConfig {
  providers: Array<Provider | EnvironmentProviders>;
}

A fresh ng new ships this:

import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZonelessChangeDetection(),
    provideRouter(routes),
  ],
};

That is the entire configuration of a default Angular app: zoneless change detection, plus a router with an empty route table. Every feature you add — HTTP, forms, animations, error handlers, custom services — lands as another element in this providers array.

The rule is simple: anything app-wide goes here, anything component-local goes in the component's own providers.

The provideX() family #

Angular's standalone API exposes a function for every framework feature you might want app-wide. These functions all return an EnvironmentProviders value — a bundle of provider registrations the injector can consume. You call the function with options and drop the result into the providers array.

The canonical set, in roughly the order you would add them as your app grows:

Function Package What it enables
provideZonelessChangeDetection() @angular/core Modern signal-driven change detection (default in v21+).
provideRouter(routes, ...features) @angular/router The Angular router. Features include withInMemoryScrolling(), withViewTransitions(), withComponentInputBinding().
provideHttpClient(...features) @angular/common/http The HTTP client. Features include withFetch(), withInterceptors([...]), withXsrfConfiguration().
provideAnimationsAsync() @angular/platform-browser/animations/async Async-loaded animations package — required for Angular Material, optional for custom animations.
provideClientHydration() @angular/platform-browser Non-destructive hydration after SSR. Pair with provideServerRendering() on the server.
provideExperimentalZonelessChangeDetection() @angular/core Legacy name for provideZonelessChangeDetection(), kept for migration projects on the old API.
provideExperimentalCheckNoChangesForDebug() @angular/core Dev-only — catches ExpressionChangedAfterItHasBeenCheckedError early.
provideAppInitializer(fn) @angular/core Runs an async function before the app boots — config fetches, auth checks, feature flags.
provideErrorHandler(handler) @angular/core Custom global error handler.

A realistic mid-sized app's app.config.ts looks more like this:

import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding, withViewTransitions } from '@angular/router';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { provideClientHydration } from '@angular/platform-browser';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { routes } from './app.routes';
import { authInterceptor } from './core/auth.interceptor';
import { errorInterceptor } from './core/error.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZonelessChangeDetection(),
    provideRouter(
      routes,
      withComponentInputBinding(),
      withViewTransitions(),
    ),
    provideHttpClient(
      withFetch(),
      withInterceptors([authInterceptor, errorInterceptor]),
    ),
    provideAnimationsAsync(),
    provideClientHydration(),
  ],
};

Reading that file tells you, in twenty lines, what the app supports app-wide. There is no AppModule to grep through, no forRoot() to track down, no "why is this provider tree-shaken?" mystery. It is just a list.

Adding your own providers #

The providers array also takes custom providers. The four shapes you will use most:

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),

    // 1. Class provider (shorthand) — registers AnalyticsService as a singleton
    AnalyticsService,

    // 2. Class provider (explicit)
    { provide: LoggerService, useClass: ProductionLoggerService },

    // 3. Value provider — useful for config objects, environment values
    { provide: API_BASE_URL, useValue: 'https://api.example.com' },

    // 4. Factory provider — when the value needs computation
    {
      provide: FEATURE_FLAGS,
      useFactory: () => loadFeatureFlagsFromCookie(),
    },
  ],
};

The API_BASE_URL and FEATURE_FLAGS tokens are InjectionToken instances — the typed way to register non-class values. Lesson 1.7 (DI Deep Dive) covers the full provider grammar, including useExisting, multi-providers, and the @Optional() / @Self() / @SkipSelf() decorators. For today, the four shapes above cover 95% of what you will write.

Environment-aware configuration #

A common pattern: dev vs staging vs production differ on a few values (API base URL, log level, feature flags) and otherwise share everything. Angular CLI's fileReplacements feature swaps an environment.ts file at build time, but a leaner pattern in 2026 is to use a single appConfig and read the environment from a typed token:

// app.config.ts
import { isDevMode } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    { provide: API_BASE_URL, useValue: isDevMode() ? 'http://localhost:3000' : 'https://api.example.com' },
    { provide: LOG_LEVEL,    useValue: isDevMode() ? 'debug' : 'warn' },
  ],
};

isDevMode() is true when the bundle was built with the development configuration; the production build constant-folds the condition and tree-shakes the dev branch. No file replacement needed for this many values.

For larger config blocks, you still use fileReplacements in angular.json to swap whole environment.ts files — but for two or three knobs, isDevMode() is cleaner.

What appConfig is not for #

Not everything app-related belongs in ApplicationConfig. The line is sharp:

Belongs in appConfig Does NOT belong in appConfig
App-wide HTTP, router, animations A single feature's local services (put them on that feature's component or route)
Cross-cutting interceptors A modal's view-providers
App-wide error handlers, app initializers Route guards (they live on the Route config, not appConfig)
InjectionTokens for app-wide config values Per-component state (use signals on the component class)
Service mocks during SSR Lazy-loaded feature providers (lazy routes pass their own providers)

Lazy routes can have their own providers arrays — lesson 5.6 covers route-level providers in depth. The mental model: appConfig is the root injector; lazy routes and components can layer their own injectors on top.

SSR splits the bootstrap in two #

When you add SSR (ng add @angular/ssr), Angular generates a sibling file:

src/
├── main.ts             ← Browser bootstrap (uses appConfig)
├── main.server.ts      ← Server bootstrap (uses appConfig.server.ts)
├── server.ts           ← Express adapter
└── app/
    ├── app.config.ts          ← Shared config
    └── app.config.server.ts   ← Server-only providers (mergeApplicationConfig)

The server config merges into the browser config — it does not replace it. The shape:

// app.config.server.ts
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';

const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering(),
  ],
};

export const config = mergeApplicationConfig(appConfig, serverConfig);

Anything in appConfig is shared. Anything server-specific (TransferState, server-rendering provider) lives in appConfig.server.ts. Module 9 (SSR Performance) walks the server side in depth.

Bootstrap error handling #

That .catch((err) => console.error(err)) at the end of main.ts is not decoration. Errors during bootstrap — a missing provider, a malformed route, a service that throws in its constructor — propagate as a rejected promise. By default they would silently disappear without that catch.

In production you usually replace the bare console.error with a real reporter:

bootstrapApplication(App, appConfig)
  .catch((err) => {
    Sentry.captureException(err);
    document.body.innerHTML = '<p>Application failed to start. Refresh to retry.</p>';
  });

This is the only error-handling site that fires before Angular's own ErrorHandler is alive, so it has to stand on its own.

Common gotchas #

Symptom Cause Fix
NullInjectorError: No provider for HttpClient! You called inject(HttpClient) somewhere but did not register provideHttpClient() in appConfig. Add provideHttpClient() to the providers array.
Function expressions are not supported in decorators in Angular AOT (older error) You used a factory in useFactory that the AOT compiler cannot statically analyze. Pull the factory out into a named function and reference it.
Provider works in dev, fails in production build The provider returned a class that was tree-shaken because nothing imported it directly. Import the class in app.config.ts (or wherever it is provided), even if just for the side effect.
provideRouter called but no routes match The routes array is empty or the path patterns don't match the URL. Confirm routes is exported from app.routes.ts and contains at least one entry.
provideClientHydration set but page flashes empty on hydrate You added the provider but did not add SSR rendering on the server side. Either run ng add @angular/ssr to wire both halves, or remove the client-side hydration provider.

What's next #

Lesson 1.3 introduces dependency injection from the consumer's side — how a service or component receives a value the injector knows about. We have spent this lesson on the registration side; lesson 1.3 is the consumption side (inject(), constructor parameters, runInInjectionContext). Together they are the two halves of how Angular wires anything to anything.

Lesson 1.7 then takes DI several layers deeper — InjectionToken<T>, optional and self-scoped lookups, multi-providers, and the hierarchical-injector resolution rules. Save that one for after you have written some real services.

Try it yourself #

The sharpest way to feel appConfig is to add a provider, consume it, and watch the bundle shake the inactive provider out:

// app.config.ts
import { InjectionToken } from '@angular/core';

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    { provide: GREETING, useValue: 'Hello from appConfig!' },
  ],
};
// app.ts
import { Component, inject } from '@angular/core';
import { GREETING } from './app.config';

@Component({
  selector: 'app-root',
  template: `<h1>{{ greeting }}</h1>`,
})
export class App {
  greeting = inject(GREETING);
}

If an AI assistant is wired to Angular's MCP server, asking it the right question is even faster:

YouI need my app to call an API and animate a Material dialog. What do I add to app.config.ts?
Claude · used find_examplesTwo functions to add to your providers array:

provideHttpClient(withFetch()) from @angular/common/http — for API calls
provideAnimationsAsync() from @angular/platform-browser/animations/async — required by Angular Material

Also run ng add @angular/material separately to install Material itself; the schematic will add what it needs on top of what you have.

That is appConfig end-to-end. The next lesson picks up dependency injection from the consumer side — inject(), constructor params, and how Angular's hierarchical injectors actually find a value.

Up next in Angular

More from this topic

View all Angular articles →
Angular

When Angular is launched ?

Link copied When Angular is launched ? When Angular Landed: A History of the Popular Web Framework # Angular, …

Feb 8, 2024 Read →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email stays private. Required fields are marked *

Leave a Comment

Your email stays private. Required fields are marked *