skip to content

For a new Angular 22 app with routing, an HttpClient auth interceptor and SSR hydration, what goes into app.config.ts, and why?

level: middleimportance: should knowfreq 58%

answer

  1. one provideX per feature
  2. withX arguments add features
  3. defaults removed older lines
  4. EnvironmentProviders guard placement

basics

~10 s

app.config.ts lists provideBrowserGlobalErrorListeners(), provideRouter(routes), provideHttpClient(withInterceptors([auth])) and provideClientHydration(withEventReplay()); zoneless, fetch and incremental hydration are v21-v22 defaults, so no extra lines are needed for them.

solid answer

~30 s

In Angular 22 the `providers` array holds `provideBrowserGlobalErrorListeners()` (CLI default), `provideRouter(routes)`, `provideHttpClient(withInterceptors([authInterceptor]))` and `provideClientHydration(withEventReplay())`. Several lines from older configs are gone: zoneless is the default since v21, so there is no zone provider; `FetchBackend` is the default since v22, so no `withFetch()`; and incremental hydration is on by default with `provideClientHydration`. The functions replace `RouterModule.forRoot` and the deprecated `HttpClientModule` because optional behaviour comes in as separately imported `withX()` features, which keeps unused ones out of the bundle, and because they return `EnvironmentProviders`, which a component's `providers` rejects.

code

ts · 15 lines
ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { routes } from './app.routes';
import { authInterceptor } from './core/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes, withComponentInputBinding()),
    provideHttpClient(withInterceptors([authInterceptor])),
    provideClientHydration(withEventReplay()),
  ],
};

go deeper

for a junior

Know the four calls a routed, server-rendered app with HTTP needs, and which package each provideX function is imported from.

for a middle

Explain the feature-function pattern: withX arguments, EnvironmentProviders, and which lines older configs had that current defaults made unnecessary.

for a senior

Review a config for stale lines left by upgrades, such as no-op withFetch or an unintended zone provider, and know when array order matters because tokens collide.

for a principal

Decide which cross-cutting concerns belong in the root config versus route-level providers, keeping the root file small enough that a reviewer can audit every enabled feature.

## The scenario A new Angular 22 app needs three things wired at the application level: **routing**, **HttpClient** with an auth interceptor, and **client hydration** because the pages are server-rendered. In a standalone app all three are provider functions listed in `app.config.ts`: ```ts import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; import { routes } from './app.routes'; import { authInterceptor } from './core/auth.interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), provideRouter(routes), provideHttpClient(withInterceptors([authInterceptor])), provideClientHydration(withEventReplay()), ], }; ``` ## Line by line - **`provideBrowserGlobalErrorListeners()`** — generated by the CLI in every new project's config; it routes uncaught browser errors to Angular's `ErrorHandler`. - **`provideRouter(routes)`** — registers the `Router` and its services with the route table. Optional behaviour is added with `withX()` feature functions as extra arguments, for example `withComponentInputBinding()`. - **`provideHttpClient(withInterceptors([authInterceptor]))`** — configures `HttpClient` and registers a functional interceptor. - **`provideClientHydration(withEventReplay())`** — tells the browser app to reuse the server-rendered DOM instead of discarding it; `withEventReplay()` captures clicks made before hydration finishes and replays them afterwards. ## What is deliberately *not* in the file The v20–v22 defaults removed several lines that older tutorials show: | Line from older configs | Why it is absent in v22 | |---|---| | `provideZoneChangeDetection({ eventCoalescing: true })` | Zoneless is the default since v21; add a zone provider only to opt back in | | `withFetch()` inside `provideHttpClient` | `FetchBackend` is the default since v22; `withFetch()` is deprecated and does nothing | | `withIncrementalHydration()` | Incremental hydration is on by default with `provideClientHydration` since v22; the function is deprecated | | `importProvidersFrom(HttpClientModule)` | `HttpClientModule` is deprecated in favour of `provideHttpClient` | | `RouterModule.forRoot(routes)` | Replaced by `provideRouter(routes)` in standalone apps | A related v21 change: `HttpClient` is injectable **without** any provider. `provideHttpClient()` is still how you configure it — here it is needed because of the interceptor. ## Why functions instead of modules The module era configured the same things with `RouterModule.forRoot(routes, { ... })` and `HttpClientModule`. The function-based APIs improve on that in several ways: 1. **Composable features.** Each optional behaviour is its own `withX()` function passed as an argument, so the config lists exactly what is enabled, and each function is typed for the provider it belongs to. 2. **Unused features stay out of the bundle.** A feature you never import and call is never referenced, so the bundler can leave its code out. An options-object API has to ship code for every option it might read. 3. **Guarded placement.** These functions return **`EnvironmentProviders`**. A component's `providers` array rejects them with `NG0207`, so app-wide infrastructure cannot be accidentally re-created per component. 4. **No module graph to reason about.** There are no `forRoot`/`forChild` pairs and no risk of importing a root-level module twice. ## Does the order of the array matter? Rarely. Each function registers providers for different tokens, so their order is irrelevant. Order matters only when **two entries provide the same token**: the injector keeps the **last** registration for a non-multi token, while multi-providers accumulate. That is worth knowing when a server config is merged on top of this one. ## A review checklist for an existing `app.config.ts` Configs that have lived through several upgrades collect lines that no longer earn their place. When reviewing one against v22, check each entry: - **Is it a no-op?** `withFetch()` now does nothing; `withIncrementalHydration()` is deprecated because the behaviour is already on. - **Is it an opt-out in disguise?** `provideZoneChangeDetection()` is not a harmless leftover — it re-enables zone-driven change detection. Keep it only on purpose. - **Is it a deprecated module?** `importProvidersFrom(HttpClientModule)` should become `provideHttpClient(...)`. - **Does it need to be global?** A provider used by one lazily loaded feature can move to that route's `providers`, shrinking what every page pays for at startup. - **Is it duplicated?** Two entries for the same token mean the later silently wins, which usually hides a mistake. The `ng update` migrations handle many of these mechanically, but a human still needs to decide the zone question and the global-versus-route question. ## Where the details live This file only shows *which* functions to call. The meaning of each router feature, each HttpClient feature and the hydration options are separate subjects; interviewers asking about `app.config.ts` usually want the list, the reason each line is there, and awareness of the defaults that removed lines.

  • A colleague adds provideZoneChangeDetection() and withFetch() to this v22 config out of habit; what do you tell them?
    `withFetch()` is deprecated and a no-op, because `FetchBackend` is already the default; remove it. `provideZoneChangeDetection()` is not a no-op: it opts the app back into zone.js-driven change detection, which v21 made optional. Keep it only if the app genuinely still relies on zone-triggered checks.
  • If HttpClient is injectable without a provider since v21, why keep provideHttpClient here?
    Because the app needs a feature. Interceptors, XSRF settings, the XHR backend and similar options are all configured as `withX()` arguments to `provideHttpClient`. Without features you could drop the call; with an auth interceptor it is where the configuration lives.

An app.config.ts is like a building's switchboard with one breaker per service: you flip on routing, HTTP and hydration individually, and each sub-feature is its own labelled switch rather than a dial on one master unit.

saying these in an interview costs you the question

  • provideHttpClient(withFetch()) is required for HttpClient to use fetch in v22
  • Every new app config must include provideZoneChangeDetection()
  • Incremental hydration needs withIncrementalHydration() added in v22
  • RouterModule.forRoot(routes) goes in the providers array of a standalone app
  • The order of provideX calls in the array usually changes behaviour