24 Sep 2024
6 min

Dynamic Configuration: Leveraging APP_INITIALIZER

In Angular applications, there are two main approaches to handling configuration settings: using environment.ts files and using the APP_INITIALIZER token. At first glance, both methods seem to do the same thing: store the values the application needs to run. In fact, there are significant differences between them:

  1. Using environment.ts Files: Configuration values are included directly in the build of your application, meaning they are hard-coded into the final bundle. This approach ensures the application starts up quickly, but it has limitations. Large configurations can increase the bundle size, and any changes to the configuration require rebuilding and redeploying the application.
  2. Using APP_INITIALIZER Token: This method loads configuration settings at runtime. The configuration is fetched when the application starts, allowing updates without needing to rebuild or redeploy. This approach is particularly useful for applications that require different settings for various environments or customers.

In short, environment.ts makes the configuration part of the build, which can make the bundle larger and harder to update. On the other hand, APP_INITIALIZER allows updating the configuration dynamically, making it more flexible and easier to manage. This article will focus on the second approach.

Use case

Consider a project that needs to be deployed for various customers, each with a different configuration. To maintain a common codebase, the application should load the configuration at runtime. It is supposed to design the possibility to update the configuration without changing the application code. In other words, it should be possible to modify the configuration without the need to rebuild or redeploy the application. In given example, the configuration will include the following:

  • company name
  • api url – every customer uses different api hosts
  • theme – the default ui theme
  • language
  • enabled features such as chat, payment and notifications
  • support email

The use of the APP_INITIALIZER token will be explored as a solution for the given requirements, with a comparison to the traditional environment.ts file approach. Additionally, the article demonstrates how to validate the configuration using Zod.js to ensure it meets all of the application’s needs.

APP_INITIALIZER Token

Let’s take a closer look at what APP_INITIALIZER is. According to the official documentation, it is:

A DI token that you can use to provide one or more initialization functions.

The provided functions are injected at application startup and executed during app initialization. If any of these functions returns a Promise or an Observable, initialization does not complete until the Promise is resolved or the Observable is completed.

You can, for example, create a factory function that loads language data or an external configuration, and provide that function to the APP_INITIALIZER token. The function is executed during the application bootstrap process, and the needed data is available on startup.”

APP_INITIALIZER is a special token in Angular allowing one or more functions to be run when the application starts. These functions are called during the initialization of the app. For example, you could create a function that loads configuration from a JSON file. By using APP_INITIALIZER, this function will run automatically when the app starts, making sure the configuration is ready to use right from the beginning.

With the role of APP_INITIALIZER understood, the next step is to demonstrate the implementation of dynamic configuration loading.

Implementing Dynamic Configuration with APP_INITIALIZER

Basically, an example solution contains the following steps:

  1. At startup, the application will fetch the config.json file.
  2. The content of the configuration will be parsed and passed to a dedicated service.
  3. From then on, it can be retrieved using the service and processed as needed.

Let’s take a look at the starting point of the application, which is the main.ts file:

function initializeAppFactory(
  httpClient: HttpClient,
  configService: ConfigService
) {
  const url = './config.json';
  return () =>
    httpClient.get(url).pipe(
      tap((config) => {
        const dto = parseDTO(config);
        if (dto.success) {
          configService.setConfig(dto.data);
        } else {
          console.error('Invalid config.json', dto.error);
        }
      })
    );
}


bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(),
    {
      provide: APP_INITIALIZER,
      useFactory: initializeAppFactory,
      multi: true,
      deps: [HttpClient, ConfigService],
    },
  ],
});

The code defines and uses a factory function (initializeAppFactory) to load a configuration file (config.json) during the application’s startup. This ensures that the app is correctly set up before it starts running.

The initializeAppFactory function is called by Angular during the initialization phase. It injects two services: HttpClient and ConfigService. The HttpClient service is used to make an HTTP GET request to fetch the config.json file. Once the file is fetched, its content is parsed using the zod.js library. This approach ensures that the configuration is correct and reliable before the app begins operating. Please refer to the article on parsing and mapping API responses using zod.js. In it, you’ll learn how to optimize your code for better API communication. The same approach can be applied to parsing configuration files. In short, zod.js allows you to define a schema for the expected data, validate whether the given data meets all requirements, and infer the type from the schema. The implementation is presented in the code:

import { z } from 'zod';
// Define the schema for the config
const schema = z.object({
  companyName: z.string(),
  apiUrl: z.string(),
  theme: z.string(),
  language: z.string(),
  features: z.object({
    enableChat: z.boolean(),
    enablePayments: z.boolean(),
    enableNotifications: z.boolean(),
  }),
  supportEmail: z.string(),
});


// Infer the type from the schema
export type ConfigDTO = z.infer<typeof schema>;


// Parse the config if matches the schema
export function parseDTO(source: unknown) {
  return schema.safeParse(source);
}

Going back to initializeAppFactory, if the DTO is successfully parsed, the configuration will be passed to ConfigService. In this case, the service’s role is straightforward – it stores the configuration and makes it available for use throughout the application. It’s essentially a wrapper around the APP_INITIALIZER token. As the application and configuration grow, the service could be extended with specific methods to retrieve particular parts of the configuration.

@Injectable({
  providedIn: 'root',
})
export class ConfigService {
  #config!: ConfigDTO;


  setConfig(config: ConfigDTO) {
    this.#config = config;
  }


  getConfig() {
    return this.#config;
  }
}

Finally, in any part of the app, the ConfigService can be injected, and the configuration can be used:

config = inject(ConfigService).getConfig();

Config.json

Let’s look at an example of a config.json file. It’s a basic JSON file that can store any data needed to run the application. It’s important that the data structure matches the schema and can be correctly parsed by zod.js.

{
  "companyName": "Customer One",
  "apiUrl": "https://api.customer1.com/v1/",
  "theme": "dark",
  "language": "en-US",
  "features": {
    "enableChat": true,
    "enablePayments": false,
    "enableNotifications": true
  },
  "supportEmail": "support@customer1.com"
}

A key benefit is that the file is not included in the bundle during development or after the build process. This means it can be easily updated even after the app is built (for example, directly on the server where the application is hosted). Any changes made to this file will be reflected immediately without needing to rebuild the app, and the JavaScript files can remain untouched. It also allows you to deploy the same bundle with different configurations. Below is the output of the build process.

Comparison with environment.ts Files

As mentioned in the introduction, Angular provides an alternative approach for managing application configuration through the environment.ts file. However, can this approach achieve the same flexibility as using the APP_INITIALIZER token? The answer is no. The environment.ts file is bundled during the build process, meaning its contents are hard-coded into the final application bundle. This limitation makes it impossible to update or change the configuration after the application has been built and deployed. In contrast, the APP_INITIALIZER token allows you to fetch configuration data dynamically at runtime, enabling you to modify application behavior without needing to rebuild or redeploy the application. This makes APP_INITIALIZER particularly useful for applications that require different configurations for different deployment environments.

Downsides of Using APP_INITIALIZER_TOKEN in Angular

Using APP_INITIALIZER_TOKEN can be very useful, but it has some downsides. One major issue is that if you use it to fetch data from an external service, any delays or problems with that service can hold up your app from starting. For example, if the service is slow or has issues, your app might be delayed or even fail to start.

However, in the example from this article, a local JSON file is loaded (one that’s on the same server as your app). In this case, delays are usually not a problem. Since the file is local, it loads quickly and network delays are minimal. Therefore, the delay in loading the JSON file is unlikely to be a major issue.

Conclusion

The APP_INITIALIZER token is a highly effective way to dynamically configure Angular applications at runtime. By loading a configuration file like config.json when the app starts, you can easily manage configuration such as API URLs, themes, default language, and feature options without needing to change the application code. This approach is more flexible than using the traditional environment.ts file because it allows you to update configuration even after the app is built and deployed. This makes it easier to customize the application for different customers without having to rebuild or redeploy the app. Additionally, using Zod.js for validation ensures that the configuration data is accurate and reliable. Overall, this method simplifies the process of managing multiple configurations and makes it easier to maintain a single codebase across different customer setups.

Source code: https://github.com/maciejkoch/config-example

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.