Changing Angular environment variables at runtime

One of my biggest frustrations with using Angular in a continuous-integration/continuous-deployment (CI/CD) pipeline is the lack of flexibility when dealing with environments outside of the Angular CLI.

Out of the box, the Angular framework does come with environment support. For example, you'll find the environment.ts file useful when working against development and building for production. But this feature falls short if your pipeline has more than two environments or you want to 'promote' builds up through environments rather than rebuilding.

What do I mean? Well, the environment.ts values are used at build time (when the command npm run build) is executed. That means you cannot deploy the same compiled code to a development server and your live production server.

I have tried and tested a more preferable approach at my workplace for over six months. It uses the APP_INITIALIZER feature from Angular to dynamically load environment variables when the app is first opened in the browser. This allows you to load environment variables from a separate file, API and configure them on a per-environment basis.

In this post, I'll show you how you can set it up in your project too…

How to use dynamic environment variables in an Angular app

This guide will assume you have some basic familiarity with Angular, and already have a project set up.

Step 1:
Start by creating a file that will store your environment variables. JSON files work best, and can later easily be swapped for an API endpoint. So, create a file called: assets/app-config.json.

Add a key-property value. In this example we'll use servicesBaseUrl. The app-config.json will now look like this:

{
    "servicesBaseUrl": "http://localhost:8000/api"
}

Step 2:
Next create a service to use the app-config.json file throughout your application. I reccommend naming it AppConfigService.

Here is some boilerplate code for the service:

import { Injectable, Injector } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class AppConfigService {
    private appConfig;

    constructor (private injector: Injector) { }

    loadAppConfig() {
        let http = this.injector.get(HttpClient);

        return http.get('/assets/app-config.json')
        .toPromise()
        .then(data => {
            this.appConfig = data;
        })
    }

    get config() {
        return this.appConfig;
    }
}

As shown above, add two methods. The loadAppConfig() method loads the app-config.json file created in Step 1 and assigns the values to a class variable called appConfig. This means the data remains in memory for as long as your Angular app is running.

The config() getter method gives a clean API surface for this service when retrieving values from the app-config.json file.

Note: The Injector service is used to load the HttpClient service because this AppConfigService will be bootstrapped when the app is launched. As a result, we cannot always rely on the Angular Dependency Injection module to be ready and so Injector allows us to get around this.

Step 3:
Now the Angular app needs to be configured to execute the loadAppConfig() method when the app is loaded in the browser.

To do this, head to the app.module.ts file and add the following code to the providers array as shown below:

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AppConfigService } from './services/app-config.service';

@NgModule({
    ...
    providers: [
        AppConfigService,
        {
            provide: APP_INITIALIZER,
            useFactory: appInitializerFn,
            multi: true,
            deps: [AppConfigService]
        }
    ],
    ...
})
export class AppModule { }

In the code above, we have told the Angular app that it must execute a function called appInitializerFn() when the application is initialized. We have also told it that it will need access to the AppConfigService as a dependency when executing the appInitializerFn() function.

Step 4:
Now define the appInitializerFn(). It can be done in the same file, or a separate file depending on how you want to structure your code.

Simply create the appInitializerFn() which returns a function which returns a Promise like so:

const appInitializerFn = (appConfig: AppConfigService) => {
    return () => {
        return appConfig.loadAppConfig();
    }
};

Step 5:
The final step is to actually use the environment variables within your Components or Services. Here is an example Service which imports the AppConfigService and uses it to initialise the API base path:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AppConfigService } from './services/app-config.service';

@Injectable()
export class DataContextService {
    basePath: string;

    constructor (private environment: AppConfigService, private http: HttpClient) {
        this.basePath = environment.config.servicesBasePath;
    }

    getNames() {
        return this.http.get(this.basePath + '/names/');
    }
}

How to load environment variables from an API endpoint

In Step 2 we loaded the environment variables from the app-config.json file. Since we already use the HttpClient to make a request to the /assets directory, we can easily change this to load variables from a database via a REST API.

To do this, change the loadAppConfig() method in the AppConfigService to query an endpoint instead.

loadAppConfig() {
    let http = this.injector.get(HttpClient);

    return http.get("https://my-api.com/path-to-endpoint")
    .toPromise()
    .then(data => {
        this.appConfig = data;
    });
}

A reminder that if this endpoint is slow, unresponsive or unavailable your Angular app may not load. Keeping environment variables local to the project (as with the app-config.json file in the assets/ directory) is a recommended practise.

Subscribe to Email Updates