In an Angular SSR app, a provider override added to app.config.server.ts never takes effect; how does mergeApplicationConfig combine configs, and what would you check?
answer
- left-to-right reduce
- providers arrays concatenated
- last regular provider wins
- multi providers accumulate
basics
~20 smergeApplicationConfig concatenates providers arrays left to right without de-duplicating; the injector then keeps the last regular provider per token and accumulates multi providers. Check argument order (shared first, server second), multi tokens, and closer injectors.
solid answer
~30 s`mergeApplicationConfig(...configs)` is a left-to-right reduce: it shallow-assigns top-level fields and concatenates the `providers` arrays. Nothing is de-duplicated; the root injector resolves conflicts, keeping the last registration for a regular token and collecting every `multi: true` entry. So the CLI's `mergeApplicationConfig(appConfig, serverConfig)` lets server entries override shared ones. When an override is ignored, check that the arguments are not reversed, that the token is not a multi-provider (appending adds rather than replaces), that a component or route injector is not providing the same token closer to the consumer, and that the override is in the config the server actually bootstraps with.
code
ts · 15 linesimport { ApplicationConfig, mergeApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
import { STORAGE, MemoryStorage } from './core/storage';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(withRoutes(serverRoutes)),
{ provide: STORAGE, useClass: MemoryStorage },
],
};
// Shared first, server second: for a regular token the later provider wins.
export const config = mergeApplicationConfig(appConfig, serverConfig);go deeper
Know that a server-rendered app has a shared app.config.ts and a server file that combines it with server-only providers through mergeApplicationConfig.
Explain that the merge concatenates providers in argument order and that the injector keeps the last regular provider but all multi providers.
Debug an ignored override systematically: argument order, multi tokens, closer component or route injectors, and which entry point uses which config.
Set a convention for platform-specific configs, shared first and platform file second, so overrides are predictable and browser-only factories never reach the server.
## How the server config is built A server-rendered Angular app has two configuration files. `app.config.ts` holds the providers shared by browser and server. `app.config.server.ts` adds what only the server needs. The CLI generates the server file like this: ```ts import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; import { provideServerRendering, withRoutes } from '@angular/ssr'; import { appConfig } from './app.config'; import { serverRoutes } from './app.routes.server'; const serverConfig: ApplicationConfig = { providers: [provideServerRendering(withRoutes(serverRoutes))], }; export const config = mergeApplicationConfig(appConfig, serverConfig); ``` The merged `config` is what the server's bootstrap function passes to `bootstrapApplication`. ## What `mergeApplicationConfig` actually does `mergeApplicationConfig(...configs)` from `@angular/core` is a small reducer. Working **left to right**, it: 1. Shallow-copies each config's top-level fields onto the result (with `Object.assign`). 2. **Concatenates** the `providers` arrays: `[...previous.providers, ...current.providers]`. It does **not** de-duplicate, deep-merge or resolve conflicts. Resolving conflicts is left to the injector that later receives the combined array. ## How the injector resolves duplicates When the root environment injector processes the combined list: | Situation | Result | |---|---| | Two regular providers for the same token | The **last** one registered wins; the earlier record is overwritten | | Two `multi: true` providers for the same token | Both are kept; injecting the token yields an array of both values | | One regular and one `multi: true` provider for the same token | Development mode throws "Cannot mix multi providers and regular providers" | So the argument order of `mergeApplicationConfig` is the override order: **the later config wins**. ## Diagnosing an ignored server override The symptom: a team adds `{ provide: STORAGE, useClass: MemoryStorage }` to `serverConfig` so that server rendering stops touching `localStorage`, but the server still crashes inside the browser implementation. Things to check, in order: - **Argument order.** `mergeApplicationConfig(serverConfig, appConfig)` puts the browser provider last, so it wins. The CLI order, shared config first and server config second, is the one that makes server overrides work. - **Multi-provider.** If the shared entry is `multi: true`, adding another entry does not replace it; both run. Overriding a multi-token means removing the shared entry from the list, not appending. - **Wrong injector.** An override in the root config cannot replace a provider declared in a component's or route's `providers`, because the closer injector answers first. - **Wrong file.** The browser entry point uses `appConfig` alone; only the server's bootstrap uses the merged `config`. An override placed in `app.config.ts` affects both sides. ## The mirror problem: browser-only code in the shared config `app.config.ts` runs on **both** platforms, so every provider factory in it runs on the server too. A factory that reads `window` or `localStorage` at construction time breaks server rendering. Two clean fixes: - Keep the shared config platform-neutral and add the browser-specific provider in a separate browser config, merged in `main.ts` with `mergeApplicationConfig(appConfig, browserConfig)` — the same pattern the server file uses. - Or make the implementation itself platform-aware, deferring browser API access until it is actually needed. ## Why the merge is so simple It is tempting to expect a helper named "merge" to understand providers — to spot that two entries target the same token and keep the "right" one. `mergeApplicationConfig` does not, and the plain behaviour has practical upsides: - **Provider bundles are opaque.** A `provideX()` call returns an `EnvironmentProviders` object whose provider list sits in an internal field, so a token-aware merge would have to reach into private structure. - **The injector already has a rule.** Last-registration-wins for regular tokens and accumulation for multi tokens is the same rule that governs any providers array. Keeping the merge as plain concatenation means one rule applies everywhere, and the result is identical to writing both lists into a single array by hand. - **More than two configs work the same way.** `mergeApplicationConfig(a, b, c)` accepts any number of configs, and precedence is simply their position. The price is that ordering mistakes fail silently. A quick diagnostic is to inject the token in a server-rendered component and log the constructor name of what arrives, or to set a breakpoint in the override's constructor: if it never runs, the override lost the merge. ## What to remember - `mergeApplicationConfig` concatenates provider lists **in argument order**; it is not a smart merge. - For a regular token, the last provider wins; for a multi token, all are kept. - Put the shared config **first** and the platform-specific config **second**. - Anything in `app.config.ts` is also server code.
- How would you give the browser a provider the server must never construct?Keep `app.config.ts` platform-neutral, create a `browserConfig` with the browser-only provider, and bootstrap the browser with `mergeApplicationConfig(appConfig, browserConfig)` in `main.ts`. The server keeps merging `appConfig` with its own config, so the browser factory never runs during server rendering.
saying these in an interview costs you the question
- mergeApplicationConfig de-duplicates providers by token
- The first config passed to mergeApplicationConfig takes precedence
- Appending a second multi provider replaces the first one
- Providers in app.config.ts only run in the browser
- mergeApplicationConfig deep-merges nested provider options