This is an updated version for modern Angular (standalone, provider function) of my initial article Angular: dynamically load config file before app starts.
Configuration values can be added in the different environments files environment.test.ts, environment.prod.ts, environment.<YOUR_ENV>.ts. But the environment files used by your application is defined at build time.
In this article, I'll show different ways on how to fetch dynamic configuration data from an external file and load the data before the application starts. In that way, configuration values could be changed without the need of a new build.
Using provideAppInitializer()
1) Create a configuration file
Create a config file that can be hosted in the same environment where your app is deployed.
e.g. app.config.json
{
"version": "2.0.0",
"apiEndpoint":"/api/"
}2) Create a configuration service
Then, create a service that will be in charge of loading and storing the values from the retrieved config file.
This is a basic service; the properties have the same name as the one in the config file, and it contains a load() method that should return a Promise.
This method does an http call to fetch your config file, return the response as a Promise with the firstValueFrom method of rxjs , and map the config data from the JSON to our class properties in the promise resolution Object.assign(this, data).
app.config.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
// You can define an interface for the value that should be in the config file
export interface AppConfig {
version: string;
apiEndpoint: string;
}
@Injectable({
providedIn: 'root'
})
export class AppConfigService {
private http = inject(HttpClient);
public version: string;
public apiEndpoint: string;
load(): Promise<AppConfig> {
return firstValueFrom(
this.http.get('/app.config.json')
).then((data) => {
Object.assign(this, data);
return data;
});
}
}3) Call your function at application startup with provideAppInitializer()
provideAppInitializer is an Angular provided function injected at application startup and executed during app initialization. If the function returns a Promise or an Observable, initialization does not complete until the Promise is resolved or the Observable is completed. See official doc.
In the app.config.ts file:
import { ApplicationConfig, provideAppInitializer } from '@angular/core';
import { AppConfigService } from './app.config.service';
export const appConfig: ApplicationConfig = {
providers: [
// The rest of your providers
// ...
provideAppInitializer(() => {
const initializerFn = (appConfigInit)(inject(AppConfigService));
return initializerFn();
}),
]
}
// Function to call your service to retrieve the configuration value and called by provideAppInitializer
function appConfigInit(appConfigService: AppConfigService) {
return () => {
return appConfigService.load();
};
}main.ts:
import { AppComponent } from "./app/app";
import { appConfig } from "./app/app.config";
bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err));4) Using the configuration values
To use the config, simply import your service into your other services or components.
app.ts
import {AppConfigService} from './app-config.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrls: ['./app.scss']
})
export class AppComponent {
private readonly appConfigService = inject(AppConfigService);
getAppVersion() {
return this.appConfigService.version;
}
}Fetching data in main.ts before initializing Angular bootstrapping code
What if other providers depends on dynamic configuration data?
Angular does not guarantee order between multiple provideAppInitializer() calls. If you have multiple custom initializers, you need to chain them into a single initializer.
But sometimes, other libraries require you to provide configuration value when bootstrapping the application. This is the case with the Angular firebase module:
app.config.ts
import { provideFirebaseApp, initializeApp } from '@angular/fire/app';
export const appConfig: ApplicationConfig = {
providers: [
provideFirebaseApp(() => initializeApp(/*YOUR CONFIG🚨*/)),
]
}In that situation, the solution that I use is to fetch the dynamic configuration values in the main.ts, before the Angular bootstrap code kicks in, and merge the retrieved value in the environment.ts file
main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { environment } from './environments/environment';
import { AppComponent } from './app/app';
import { appConfig } from './app/app.config';
(async () => {
// Fetch the data
const response = await fetch('app.config.json');
const config = await response.json();
// Merge existing data of the environment file with the ynamically fetched ones
environment.firebaseConfig = {
...config.firebaseConfig,
...environment.firebaseConfig
};
bootstrapApplication(AppComponent, appConfig)
.catch((err) => {
console.error(err)
});
})();Use value from environment.ts in the app.config.ts file:
import { provideFirebaseApp, initializeApp } from '@angular/fire/app';
import { environment } from './../environments/environment';
export const appConfig: ApplicationConfig = {
providers: [
provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
]
}