About my blog

I write about the technical and non-technical aspects of software development

How it works

Microsoft ASP.NETASP.Net
BlogEngine.NET BlogEngine.NET
Azure DevOpsAzure DevOps

Contact info

 Email
 Contact

Follow me

Prod-20240407.1

Angular and APP_INITIALIZER

How to change your web application from compile-time configuration to runtime configuration using the APP_INITIALIZER token

Angular and APP_INITIALIZER

My goal was to shift my Angular website from a build-time or compile-time configuration to a runtime configuration so I could simplify the maintenance and improve the CI-CD options of my site. (You can read why here.)

The fundamental idea involved moving configuration out of TypeScript (the environment.*.ts files) into simple JSON or JavaScript files, which would not be part of the Angular build process. In the accompanying source code, I'm using a simple JSON-based configuration file called config.json.

The first technique I found was described in an article by Juri Strumpflohner. For want of a name, I'm calling this the " APP_INITIALIZER " approach.

The problem with moving configuration out of TypeScript is, how and when does the transpiled Angular application get it's settings, and what if those settings are needed early on in the application lifecycle?

The solution Juri describes in his post is to deliberately delay the initialisation of the application until the bootstrapping process can load a specified JSON file and inject the configuration data into the startup or bootstrapping process. This is where Angular's APP_INITIALIZER token comes in.

This is best explained by the code snippets below. (I have focused on the lines that help illustrate the technique more clearly, and removed irrelevant or boilerplate lines. Please refer to the GitHub repository for the full working code.)

In src/app/app.module.ts we define a factory method that when executed, returns the function that will load the configuration.

	
const configInitializerFactory = (configService: AppConfigService) => {
  console.debug('Entering AppModule app initializer');
  return () => {
    return configService.loadAppConfig();
  }
};

Note that the factory requires the injection of a service, AppConfigService. We define this in the file src/app/services/app-config.service.ts.

	
@Injectable({
  providedIn: 'root',
})

export class AppConfigService {
  private configFile: string = '../../assets/config.json';
  private appConfig: AppConfig = new AppConfig();

  constructor() {
  }

  loadAppConfig(): Promise<void> {
    return this.loadConfigForBrowser();
  }

  getConfig(): AppConfig {
    return this.appConfig;
  }

  private loadConfigForBrowser(): Promise<void> {
    return fetch(this.configFile)
      .then((response) => {
        console.debug(`Browser fetch config data from '${this.configFile}'`);
        return response.json();
      })
      .then((config) => {
        this.appConfig = config;
        console.debug(`App config env: ${this.appConfig.env}`);
      });
  }
}

As Juri points out in his article, it's important that the function that loads the config returns a Promise, because we want the call to await the Promise resolution before application initialisation continues.

The service is responsible for loading the configuration file, and returns the settings in a type of AppConfig, which is a simple class that mirrors the properties we're reading from the JSON.

This is the src/app/app.config.ts class:

import { InjectionToken } from "@angular/core"

export class AppConfig { env: string = "" apiBaseUrl: string = "" activeTheme: string = "" loggerLevel: string = "" buildRef: string = "" } export let APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG')

Note how the properties match up to the values configured in config.json:

{
  "env": "Dev",
  "apiBaseUrl": "https://collectionapi.metmuseum.org/public/collection/v1",
  "activeTheme": "uniconfig-A",
  "loggerLevel": "DEBUG",
  "buildRef": "1.0.0.0DEV"
}

How does Angular know when to to call the factory we saw earlier? Going back to app.module.ts, we define a provider:


providers: [ 
  AppConfigService,
  {
    provide: APP_INITIALIZER,
    useFactory: configInitializerFactory,
    multi: true,
    deps: [AppConfigService]
  }
],

So, we're telling Angular to provide the AppConfigService, (which the factory needs by dependency injection), and also told it to use the factory to provide the APP_INITIALIZER token.

So what happens?

Angular will not complete initialisation until the APP_INITIALIZER token is returned. But we've also associated the APP_INITIALIZER with a Promise that won't resolve until the configuration is loaded into an instance of AppConfig. Therefore, we can be confident that the application will get the configuration settings at start-up.

Furthermore, we can modify or replace the config.json file without rebuilding the application. This is the essence of runtime configuration!

Okay, so Angular has the configuration. But how can the components and services within the application get access to those settings?

You'll recall that there were two methods on the AppConfigService: loadAppConfig() and getConfig(). The first is called by the factory at application initialisation, and creates a new instance of AppConfig. The second however, simply returns that instance.

Therefore, in my component constructor I simply inject the AppConfigService, and in the OnInit event handler I get the configuration:

ngOnInit(): void {
  this.config = this.configService.getConfig();
  ...
}

I've added a RandomImageService to the GitHub code to illustrate the runtime configuration being used. When the project is run, you should see the page below:

Screenshot of the Angular project home page

In my next post I'll describe how I extend this mechanism for Angular Universal. But that's it for now.

Happy coding!


You Might Also Like


Would you like to share your thoughts?

Your email address will not be published. Required fields are marked *

Comments are closed