Configuring Your Angular Application to Use an External Configuration File


Normally, developers use the environment.ts and environment.prod.ts files (and maybe even other permutations) to configure their Angular applications for different environments. With a deployment system such as Octopus, symbols can be placed in these files to replace values with environment-specific ones. But what if you want to build an Angular application for use by someone outside of such an environment? I ran into this problem recently when working on an application I have up on GitHub. I wanted to be able to provide a release that someone could easily configure without having to do a find/replace inside a JavaScript file.

As long as the application is not offline, you can add a configuration file which gets distributed with your application and have the app read from it during initialization.

First, create a new configuration file. In my case, I added config.json under my src folder:


In this file, I currently just have a single configuration option: the URL to my API.

{
  "apiRoot": "http://localhost:5600/api/"
}

This file will need to be included when the app builds, so I added the file under the assets section of the project's architect build options (the last line below).

      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/workout-tracker",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json",
            "assets": [
              "src/favicon.ico",
              "src/assets",
              "src/config.json"
            ],

I created a TypeScript class so this information can be strong-typed. Sure, it's only one value now, but it could grow later.

export class Config {
  apiRoot: string;
}

Next, I used the APP_INITIALIZER token in my AppModule to make an HTTP request to get this information when the app initializes.

  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initializeApp,
      deps: [ConfigService, UserService, AuthService, HttpClient],
      multi: true
    },

You can read more about how APP_INITIALIZER works at the official docs here. In my code above, you'll see that useFactory specializes a function named initializeApp. That's a function I wrote that does the initialization, and here's what it looks like:

function initializeApp(
  configService: ConfigService,
  userService: UserService,
  authService: AuthService,
  http: HttpClient): () => Observable<any> {
  return () => http.get("config.json")
    .pipe(
      tap((config: Config) => {
        console.log("Loaded config: ", config);
        configService.init(config);
        authService.init();
        authService.restoreUserSessionIfApplicable();
        userService.init();
      })
    );
}

There's some other stuff going on here (I'd written this function before I realized I'd need to create an external config file), but the thing to notice here is the HTTP call to get config.json. Note also that the tap method specifies in its anonymous method that the config parameter is of type Config (the class we created earlier) so it's using the strong type. 

I pass the config value to my instance of ConfigService, which is a custom service in my application. This service takes the value and sets up what it needs to provide configuration to the rest of the application without relying on any tight coupling (for example, a direct reference to a file). This allows for easier unit testing.

When the application is built, the config.json file is included in the dist folder.


This allows end users to update the file with their own settings (in my case, the URL to the app's API backend).

Hopefully this helps someone!

Comments

Popular Posts

Resolving the "n timer(s) still in the queue" Error In Angular Unit Tests

How to Get Norton Security Suite Firewall to Allow Remote Desktop Connections in Windows

How to Determine if a Column Exists in a DataReader

Silent Renew and the "login_required" Error When Using oidc-client

Fixing the "Please add a @Pipe/@Directive/@Component annotation" Error In An Angular App After Upgrading to webpack 4