Loading App Configuration in the APP_INITIALIZER

Posted on October 01, 2018

angular

If you're anything like me, you've been placing the configuration for your Angular app in the environment.ts file, and then copying that same file to environment.test.ts and environment.prod.ts files. Which isn't too bad; most of those values don't change too often. But sometimes they do. And when they do, that's when the pain starts.

So for me, this is what happens:

  1. The app runs perfectly in my local development environment.
  2. I build the application and deploy to the testing environment.
  3. The application starts, but doesn't work as expected.
  4. I change an environment value, rebuild the application, and redeploy.
  5. The application starts, but doesn't work as expected.
  6. I change an environment value, rebuild the application, and redeploy.
  7. The application starts, but doesn't work as expected.
  8. I change an environment value, rebuild the application, and redeploy.
  9. ...

You get the picture. It's minor change after minor change, followed by deploys, that don't seem to fix the problem. And every build/deploy process takes 5 or 10 minutes. This translates to a lot of wasted time. And the thing is: most of those configuration values don't need to be present at build time. They can be set at runtime of the app. It's for that reason that the environment.ts files are not the place for these configuration values.

For a long time I put up with this because it was all I knew. But a few weeks ago I was listening to the Adventures in Angular podcast and they were talking about the proper location to store your app's configuration. And I was stoked! That's what I needed! They were talking with a guy named Dave Bush, who had written this blog post on the subject. I felt like I was finally understanding what I needed to do to have an effective way to introduce run-time configuration values to my application.

I got started implementing this in my app, but I didn't get too far before I was a little stumped. I couldn't find any full code examples of doing what I wanted to do. In the end, after reading a couple other articles on the subject (including this one and this one, on top of the already mentioned one above), I got a service working that loads the configuration file and provides it to places throughout the app. I've put together this StackBlitz example to show the basics of the code you'll need. Below is a brief outline as well for those steps.

Code Review

There are a few things to add to the application to load the configuration. The first is app.module.ts. Add the following function above the @NgModule decorator:

export function initConfig(appConfigService: AppConfigService) {
    return () => appConfigService.loadConfig();
}

This function simply takes the AppConfigService (which we'll look at in a minute) and returns a function calling the loadConfig function from the service. There's one really important part here though that had was causing issues for me for a few days: DO NOT FORGET TO export THE FUNCTION! I wasn't exporting that function, and I'm not sure why, but it messed up how the application builds. It doesn't produce an error, per se, but it wasn't building any lazy loaded modules. As soon as I added the export, everything built and ran as expected. So don't waste time like I did. 😃

The second addition to the application needed is a service, which I called AppConfigService. In part, that service looks like this:

constructor(private _http: HttpClient) {}

public loadConfig() {
    return this._http.get('./assets/config/config.json')
        .toPromise()
        .then((config: any) => {
            this.config = config;
            this.configSubject$.next(this.config);
        })
        .catch((err: any) => {
            console.error(err);
        })
}

public getConfig() {
    return this.config;
}

The HttpClient is necessary to be able to load the file. Also, the APP_INITIALIZER token requires a function that is a promise to be returned, so using the toPromise method on an Observable works perfectly. Another thing to point out is the configSubject$ variable that is being used. The reason I implemented this is that some of the services were loading before the file was done loading. So having them subscribe to that subject and waiting for the value ended up working perfectly for me.

Another thing that seemed to cause issues for me was not explicitly marking functions as public. I'm not sure if this is a result of using the service in the APP_INITIALIZER factory, or what, but as soon as I explicitly marked variables and functions as public, things seemed to work as expected.

Alright, now let's jump back to the app.module.ts file once more. In the providers array of the @NgModule decorator, add the following:

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

This tells the application to use the APP_INITIALIZER token, gives it a factory function to use, provides the needed dependencies, and lastly allows for using multiple APP_INITIALIZERs in your application.

After those few things are set up, you can then start using the configuration object in the service throughout your app. In the constructors for services, you can either use the getConfig method to get the configuration object or you can use the subject and subscribe to it, waiting for the configuration to be loaded. I mentioned this above, but some services were being loaded before the APP_INITIALIZER is done running. But it didn't happen every time. So, because of that, I checked multiple times in each service/module that needed to use this configuration object and determined if I needed the subject or not.

Now, there's one last step that needs to be taken to make this configuration truly run-time, and that's providing a different copy of the config.json file in each environment. In my case, we build our application and make a Docker image to run the application (see this article for more info). So to get the file in there without having to re build the application, I added a volume in the docker-compose.yml file like this: ./config.json:/usr/share/nginx/html/assets/config/config.json. This allowed me to create a config.json file on the server that was then copied into the application with new config values. You would be able to do the same thing if you cloned the application to your server and built and ran the application without putting it in a Docker container as well.

Hopefully this answers some questions for some of you. It took a few days and a lot of reading and testing to get all these pieces together, but I'm really happy with the end product. This will make changing many configuration options for our applications a lot easier, and faster too. We no longer have to go through the process of making the change, creating a PR, building new images, and deploying. It's now a simple change in a JSON file and restarting the Docker container. If this helps you out, I'd love to hear from you! Reach out and let me know!

Update

To easily achieve the functionality discussed here, use this package: runtime-config-loader. All it requires is adding the provider to your app.module.ts file, much like is described above.