In an Angular app, how would you load settings from /config.json at runtime so every service sees them before the app starts?
answer
- build once, deploy to many environments
- a config service holds the values
- initializer awaits the HTTP call
- firstValueFrom turns the request into a Promise
- alternative: fetch before bootstrapApplication
basics
~20 sPut a config service in root, give it a load() method that fetches /config.json, and register provideAppInitializer(() => inject(ConfigService).load()). Angular waits for that Promise, so the values are set before the root component and its services run.
solid answer
~40 sRuntime config lets one build artifact run in every environment: the server serves a different `/config.json` per deployment, while environment files are baked in at build time. I create a `ConfigService` in root with a private field and a `load()` method that does `firstValueFrom(http.get<AppConfig>('/config.json'))` and stores the result. Then `provideAppInitializer(() => inject(ConfigService).load())` plus `provideHttpClient()` in `ApplicationConfig`. Angular awaits the Promise before creating the root component, so components and anything they inject read a filled service. Two cautions: code that runs before initializers settle — an environment initializer, or another initializer — can still see an empty service, so I make reads fail loudly; and if the request fails, bootstrap fails, so I decide on a timeout and a fallback deliberately.
code
ts · 32 linesimport { ApplicationConfig, Injectable, inject, provideAppInitializer } from '@angular/core';
import { HttpClient, provideHttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
export interface AppConfig {
apiUrl: string;
featureFlags: Record<string, boolean>;
}
@Injectable({ providedIn: 'root' })
export class ConfigService {
private readonly http = inject(HttpClient);
private config?: AppConfig;
async load(): Promise<void> {
this.config = await firstValueFrom(this.http.get<AppConfig>('/config.json'));
}
get apiUrl(): string {
if (!this.config) {
throw new Error('ConfigService was read before /config.json finished loading');
}
return this.config.apiUrl;
}
}
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideAppInitializer(() => inject(ConfigService).load()),
],
};go deeper
Recall the pieces: a config service, a load method that fetches the file, and provideAppInitializer calling it.
Explain why runtime config beats build-time files for one-artifact deployments and exactly what Angular waits for before rendering.
Show the ordering window: early readers such as environment initializers and interceptors, plus timeouts and a failure path the user can see.
Weigh the initializer against fetching before bootstrap, and treat the extra round trip as a first-render cost across every environment.
## Why load configuration at runtime Angular's environment files are compiled into the bundle, so the API URL or feature flags they hold are fixed per build. Many teams instead want **build once, deploy many**: the same artifact is promoted from test to staging to production, and each environment serves its own `config.json` next to `index.html`. The browser then has to fetch that file before the application uses any of its values, which is exactly what an **application initializer** is for. ## The standard recipe 1. Define an `AppConfig` interface describing the file. 2. Create a `ConfigService` provided in root that holds the loaded values and exposes typed getters. 3. Give it a `load()` method that fetches `/config.json` and stores the result, returning a Promise. 4. Register `provideAppInitializer(() => inject(ConfigService).load())` in `ApplicationConfig`, together with `provideHttpClient()`. 5. Inject `ConfigService` wherever a value is needed and read it synchronously. Because Angular awaits the Promise returned by the initializer, the root component is created only after `load()` resolves. Components, and the services they inject when they are constructed, therefore see a filled service. ```ts async load(): Promise<void> { this.config = await firstValueFrom(this.http.get<AppConfig>('/config.json')); } ``` `firstValueFrom` from RxJS turns the one-shot HTTP Observable into a Promise. Returning the Observable directly would also work, because Angular waits for an Observable initializer to complete and `HttpClient` requests complete after the response. ## Reading the values Once loaded, the configuration does not change for the lifetime of the page, so consumers can read it **synchronously** through getters or a readonly signal; there is no need to expose it as an Observable that every consumer subscribes to. Typed getters also keep the file's shape in one place: when the JSON gains a field, only `AppConfig` and the service change. If a value is optional, give the getter an explicit default rather than letting `undefined` leak into URLs. ## What "before the app starts" does and does not cover The guarantee is about the **root component**. Some code runs earlier or alongside: - **Environment initializers** run when the root injector is created, before any application initializer, so anything they touch sees an empty `ConfigService`. - **Other application initializers** are called in the same pass; an initializer that reads config synchronously, or whose async work does not wait for `load()`, can run ahead of it. - **Services instantiated during the initializer phase** — for example something an HTTP interceptor injects while the config request is in flight — are constructed before the values exist, so they must read config lazily rather than copy it in a constructor. A getter that throws when the config is missing turns these ordering bugs into a clear error instead of a silent `undefined` URL. ## Two ways to make values available | Approach | How it works | Strength | Cost | |---|---|---|---| | `provideAppInitializer` + config service | Initializer awaits `load()`; consumers inject the service | Idiomatic, uses `HttpClient` and its interceptors, testable with DI | Values exist only after the phase; early readers must be careful | | Fetch before `bootstrapApplication` | `main.ts` calls `fetch('/config.json')`, then bootstraps with the result as a provided value | Config is a plain value from the very first provider; no ordering window | Outside Angular: no `HttpClient`, no interceptors, handle errors in `main.ts` yourself | The second option suits values that provider factories themselves need, such as a base URL passed into another library's setup function; the first suits most application settings. ## Making it safe - **Fail loudly or fall back deliberately.** A rejected `load()` rejects bootstrap: nothing renders and the error reaches the `ErrorHandler`. Decide whether a missing file should stop the app or fall back to defaults, and show the user something either way. - **Bound the wait.** Angular has no startup timeout; a hanging request means a blank page. An RxJS `timeout()` on the request makes it fail instead. - **Avoid caching the file.** Serve `config.json` with headers that prevent stale caching, or a deploy may run with the previous environment's values. - **Keep it small.** Every millisecond the initializer takes delays first render for every visitor. - **Do not put secrets in it.** The file is public to anyone who can open the site. ## Testing In a unit test you normally do not run the initializer at all: provide a stub `ConfigService` or set its values directly, so tests do not depend on an HTTP call happening during `TestBed` setup.
- Why not simply use Angular environment files for the API URL?Environment files are replaced at build time, so each environment needs its own build. If the team promotes one artifact through test, staging and production, the values must come from something the server supplies at runtime, such as `/config.json`, loaded before the app starts.
- An HTTP interceptor injects ConfigService in its body to prefix URLs. Does the config request itself work?The interceptor also runs for the `/config.json` request, while the service is still empty. If it reads `apiUrl` unconditionally it throws or produces a bad URL. Skip relative asset URLs in the interceptor, or load the config through a path the interceptor ignores.
- Should the initializer return the HttpClient Observable directly instead of a Promise?It can: Angular waits for an Observable initializer to complete, and an `HttpClient` request completes after its response. The Promise form with `firstValueFrom` is common because `load()` can then `await` and assign in one readable method, and it reads the same in tests.
It is like a theatre that will not open the doors until the stage manager has read tonight's cast sheet: the same production tours every city, and only the sheet changes. Staff who arrived early to set up may still be working from yesterday's sheet.
saying these in an interview costs you the question
- Environment files can change per deployment without rebuilding
- Load the config in the root component's ngOnInit
- Every service is guaranteed to see the config, even environment initializers
- Angular times out a slow initializer and starts anyway
- config.json is a safe place for API secrets