Angular external configuration using app initializer
Problem definition
Some configuration might not be known as part of development and must be configured during the deployment. Environment variables concept in Angular is good when the configuration is known but might be different in different stages of software development. Using a production or development environment is an easy way to set the values differently for these situations. But, if the configuration has a more dynamic nature and must be defined and set in production then environment variables cannot be used in that case. Lets see how this problem can be tackled in Angular.
Scenario
In this scenario there is a reusable application that is deployed for different customers. But, the customer domain must be set as a configuration as part of deployment to be able to reach the backend service.
Implementation
First, install angular cli to be able to create a new Angular application.
sudo npm install -g @angular/cli
Generate a new application for demonestrating how this problem can be solved in Angular application.
ng new angular-dynamic-config-app-initializer
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS
Generate a config module to include the service for reading the configuration.
ng generate module config
Generate the config service in the config module.
ng generate service --skip-tests config/config
Update config module and provide the new created service:
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { ConfigService } from './config.service'
@NgModule({
declarations: [],
imports: [CommonModule],
providers: [ConfigService],
})
export class ConfigModule {}
Create a json file containing the application configuration (in this case the address of the API backend service). This file is located in assets directory and can be updated as part of deployment to reflect the customer data.
code src/assets/config.json
The content.
{
"ApiBaseUrl": "https://api.myapplication.com"
}
Create an interface matching the format of data received from the config file. This interface will be used in the config service to map the data received from the json file to a variable in the config service.
mkdir src/app/config/model
code src/app/config/model/app-config.interface.ts
The content:
export interface AppConfig {
ApiBaseUrl: string
}
Next, it is important to load the configuration as part of application bootstrap process so that the configuration is in place before all other services that are dependent on this configuration are started. For that an APP_INITIALIZER is created to handle the loading of the configuration during app initialization. It will use the config service to load the configuration. A method needs to be created in the config service that reads the configuratio and returns a Promise or an Observable. Angular ensures that initialization does not complete until the Promise is resolved or the Observable is completed.
code src/app/app-initializer.factory.ts
import { ConfigService } from './config/config.service'
export function initializeAppFactory(
configService: ConfigService
): () => Promise<any> {
return async () => {
return await configService.initializeApp()
}
}
Update the config service to read the configuration from the json. HttpClient is used to get the data and some validation is added to ensure the received value is not empty. It is important that initializeApp is returning a pormise
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { firstValueFrom } from 'rxjs'
import { AppConfig } from './model/app-config.interface'
@Injectable({
providedIn: 'root',
})
export class ConfigService {
configUrl = 'assets/config.json'
config: AppConfig = { ApiBaseUrl: '' }
constructor(private httpClient: HttpClient) {}
initializeApp(): Promise<any> {
return new Promise(async (resolve, reject) => {
const response = await firstValueFrom(
this.httpClient.get<AppConfig>(this.configUrl)
)
this.config = {
ApiBaseUrl: response.ApiBaseUrl,
}
if (this.isConfigValid()) {
resolve(this.config)
}
reject(new Error('Could not get the application configuration!'))
})
}
isConfigValid(): boolean {
if (this.config.ApiBaseUrl in ['', null, undefined]) {
return false
}
return true
}
}
Now, update the App module to include the httpClient and config module and also the app initializer.
code src/app/app.module.ts
import { HttpClientModule } from '@angular/common/http'
import { APP_INITIALIZER, NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { initializeAppFactory } from './app-initializer.factory'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { ConfigModule } from './config/config.module'
import { ConfigService } from './config/config.service'
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule, AppRoutingModule, ConfigModule],
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeAppFactory,
deps: [ConfigService],
multi: true,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
Lets update the app component to show the configuration.
code src/app/app.component.ts
import { Component } from '@angular/core'
import { ConfigService } from './config/config.service'
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'angular-dynamic-config-app-initializer'
apiUrl: string = ''
constructor(private configService: ConfigService) {
this.apiUrl = configService.config.ApiBaseUrl
}
}
code src/app/app.component.html
<h1>My config</h1>
<p>API BASE URL: {{ apiUrl }}</p>
Last but not least start the dev server and open the browser to view the results.
ng serve
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
✔ Compiled successfully.
google-chrome http://localhost:4200/

The source code can be found in github