Hi everyone,
in today’s article I want to tell you about how I replaced Angular’s configuration file (the environment.ts) with an external resource (downloaded via HTTP).
The context
In recent months I’ve been developing the frontend of a very complex enterprise application, which will need to be exposed inside a Docker container, in a Kubernetes cluster. I’m writing the application in Angular (version 21), but this article can be followed for any version (with some adjustments if your Angular version is lower than version 19).
The application in question will need to be deployed on 300 different instances: using environment.ts to manage the configurations for these 300 instances is wrong because it would force us to have 300 different images in the container registry.
I therefore needed a strategy to separate the configuration from the application itself.
ProvideAppInitializer() function
Since Angular version 19, the provideAppInitializer() function has been introduced (which replaces APP_INITIALIZER, now deprecated). The function is the official way to load external resources during application startup.
Inside this function we can insert logic that returns a Promise or an Observable, so that the application bootstrap doesn’t complete until these have been resolved.
An example could be this:
//...
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideAppInitializer(loadConfiguration),
// ...
provideAppInitializer(() => {
const _configService= inject(ConfigService);
return _configService.loadConfigFromHttp();
}),
}In the example I’ve included, the provideAppInitializer() function injects a service through dependency injection and calls the loadConfigFromHttp() function, which in turn returns an object of type Observable.
With this configuration, the provideAppInitializer() function won’t complete until the Observable from the function completes.
However, this approach isn’t suitable for our use case.

Let’s now see why this solution can’t always work.
To perform login in my application I needed to use an authentication provider that was already installed (Keycloak) and to integrate it into the project there’s a convenient package to install.
Once installed it needs to be initialized in app.config.ts, exactly where we have provideAppInitializer(): the problem arises right here, because to initialize the library I need some configuration parameters, which are evaluated immediately, even before the provideAppInitializer() function has completed.
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideAppInitializer(loadConfiguration),
// ...
//Initialize auth provider
provideKeycloakAsAuthProvider(
environment.pluginSettings.keycloak.url,
environment.pluginSettings.keycloak.realm_id,
environment.pluginSettings.keycloak.client_id,
environment.apiBaseUrl,
environment.pluginSettings.keycloak.authCheck as KeycloakOnLoad,
),
// ...
}In a more “simple” application the environment.pluginSettings.keycloak.xxxx variables belong to environment.ts and are available immediately since they belong to a TypeScript class.
However, if we load the configuration via HTTP, at the moment I initialize the Keycloak provider I might not have obtained the configuration yet and therefore environment would still be empty!
My solution
We need to have the configuration loaded before the application starts and the point where we hook in the configuration file management is main.ts, which is the first file that gets executed when the application starts.
The process I implemented is as follows:
- The user opens the application and index.html starts
- First thing, the content of main.ts is executed, specifically the
bootstrap()function (see the code below) bootstrap()function:- The application downloads the configuration from an HTTP endpoint (if it’s not in cache)
- If offline –> Redirect to a static offline.html page
- If the configuration download is OK then I verify the configuration structure
- If ok I proceed with the Angular application bootstrap
- If incorrect I perform a redirect to a static error.html page
As already mentioned, code-wise everything is handled in main.ts
// ... import
/**
* Custom bootstrap workflow:
* 1. Load environment configuration from http
* 2. Inizialize configurations and providers
* 3. Startup webapp
*/
async function bootstrap(): Promise<void> {
try {
// 1. Get config from http endpoint (or from cache)
const loadResult = await loadEnvironmentConfig();
//Log
console.debug(
`[Bootstrap] Environment loaded from: ${loadResult.loadedFrom}`,
loadResult.customerKey ? `(customer: ${loadResult.customerKey})` : '(local development)'
);
if (loadResult.loadError) {
//FAILED_TO_FETCH = 'Failed to fetch' and means that my backend is unreachable
if(loadResult.loadError == responseErrors.FAILED_TO_FETCH) {
goToOfflinePage();
return;
}
console.warn('[Bootstrap] Load error:', loadResult.loadError);
goToErrorPage(loadResult.loadError);
}
// 2. Create angular config and initialize angular providers
const appConfig = createAppConfig(loadResult);
// 3. Bootstrap application
await bootstrapApplication(AppComponent, appConfig);
} catch (error) {
console.error('[Bootstrap] Critical error:', error);
goToErrorPage(error);
}
}
function goToErrorPage(error: unknown): void {
// .. Log error to my logger provider
globalThis.location.href = 'error.html';
}
function goToOfflinePage(): void {
// .. Log error to my logger provider
globalThis.location.href = 'offline.html';
}
bootstrap();
Some details about the code:
await loadEnvironmentConfig();is inside a dedicated class and handles downloading the configuration from an HTTP endpoint. Once downloaded it stores it insessionStorage, so that it’s more efficient and faster the second time.goToOfflinePage();takes the user to the offline.html pageshowCriticalErrorPage(error);takes the user to a generic error page, after tracking the error- The downloaded configuration (in JSON) is the implementation of an interface I created in the application (
IEnvironmentConfig)
export interface IEnvironmentConfig {
appMode: 'development' | 'test' | 'production';
appTitle: string;
logs: {
level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
logToConsole: boolean;
};
pluginSettings: {
keycloak: {
url: string;
client_id: string;
realm_id: string;
authCheck: 'check-sso' | 'login-required';
};
};
}{
"appMode": "development",
"appTitle": "ThinkAsADev webapp",
"logs": {
"level": "warn",
"logToConsole": false
},
"pluginSettings": {
"keycloak": {
"url": "https://xxxxxx:24443/auth",
"client_id": "my-client-id",
"realm_id": "my-realm",
"authCheck": "login-required"
}
}
}InjectionToken for configuration
When we talk about InjectionToken we’re referring to an Angular feature with which we can associate a key with an object, to then make it available throughout the application via dependency injection. Now let’s see how I associated the loaded configuration with a key (ENVIRONMENT).
To do this we need to add a parameter to the createAppConfig() function (which resides in app.config.ts)
// ...
const appConfig = createAppConfig(loadResult);
// ...// ...
export function createAppConfig(config: IEnvironmentConfig): ApplicationConfig {
return {
providers: [
provideZonelessChangeDetection(),
{
provide: ENVIRONMENT,
useValue: config,
},
//...
],
};
}This way, in any part of the application I can access my configuration through dependency injection like this:
@Injectable({
providedIn: 'root',
})
export class MyService {
private readonly _environment = inject(ENVIRONMENT);
//...
}Handling wait time
The adopted solution inevitably leads to longer application startup times, at least the first time (from the next time onwards the configuration is stored in sessionStorage()).
To handle this wait time we can add a loader inside the <app-root> tags of the index.html file: anything we insert in here will then be automatically removed by the framework and replaced with the actual Angular application.
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<title>Your app name</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head>
<body class="dx-viewport">
<app-root>
<div class="loader-container">
<img src="/images/your-logo.svg" alt="logo" class="loader-logo">
<div class="loader-spinner"></div>
</div>
</app-root>
</body>
</html>Conclusion
In this article we’ve seen how to handle dynamic loading of a configuration in an Angular application, allowing us to use the loaded parameters to initialize the various libraries in the project.
With this strategy the configurations are externalized from the application, a key strategy in enterprise applications and in those that need to be included in a container to be served.
Thanks for reading, see you next time!