How To Use Run-Time Environment Variables In Angular

The environment.ts and environment.prod.ts files in Angular had me excited when I first moved over from AngularJS. I thought "Great! I can create a file for each of my environments (local development, testing, user acceptance testing and live/production) and my application will select the correct file for the environment the app is deployed to". Since I use Octopus Deploy to deploy my binaries to multiple environments, I needed the environment variables to be determined at run-time. Unfortunately, the variables in the environment files in Angular are determined at compile-time using a flag:

ng build --env=prod

This post will walk through how to create a service which initialises when the Angular application bootstraps and loads variables from a JSON file. Of course, you can configure it to load variables from an XML file, localStorage or even a database via an API.

The benefits of this approach, for me at least, is that each environment can have it's own app-config.json file which has the variables for that particular environment. Since the service which retrieves these values initialises upon the application bootstrapping, there's no need for you (the developer) to worry about access the files and retrieving variables. If anything, it works just like importing the environment.ts file in to your code - the only difference is that it is through a service so you will need to use Angular's dependency injection.

This approach also allows you to change the values in the app-config.json file we will create and reload the application to see the changes. Using the environment.ts file requires you to re-compile your application.

Oh, and before we get going - this was tested on an Angular 5 and Angular 6 application.

How to use run-time environment and configuration variables in Angular (Angular 2, Angular 5 and Angular 6)

I will assume you already have an Angular application. I will be using the multiple-module starter I have shared on GitHub here, but any structure should work the same.

Step 1: First off, let's create the app-config.json file which will contain our environment variables. Do this by creating a new JSON file in the assets/ directory in the root of your application.

Something simple like the following should do for now:

{
    "servicesBaseUrl": "https://localhost:8080/api"
}

Step 2: Next we need to create a service to use the app-config.json file. I decided to call mine AppConfigService but you may opt for Environment if you wish. Remember, you can always import the service with an alias name…

Use the following code to get started with the AppConfigService 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;
    }
}

A few things to note. Since this service will run when your application bootstraps, we cannot rely on the other services and the Dependency Injection model to be ready. There for we have to use the Injector to access the HttpClient module.

We then define a function which simply loads the JSON data from the app-config.json file and assigns the value to the class variable appConfig.

A getter method has also been created called config. This allows us to call a function without using the brackets. In other words, we can use the config by calling AppConfigService.config.servicesBaseUrl instead of AppConfigService.config().servicesBaseUrl. This isn't essential but it feels less distant from using the environment.ts file.

If you want a fall-back option for instances when the app-config.json file cannot be loaded, you can add the following catch block in the loadAppConfig() function:

.catch(error => {
    console.warn("Error loading app-config.json, using envrionment file instead");
    this.appConfig = environment;
})

How you decide to implement your fall-back option is totally up to you. If you do use the environment.ts file, remember to import it at the top of your code - and also keep it updated during development!

Step 3: Next we need to tell our application to execute the loadAppConfig() function in the AppConfigService service when the application bootstraps. To do this, head over to your app.module.ts file (or core.module.ts file if you are using my multi-module starter from GitHub) and add the following code as appropriate:

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 { }

This code is simply importing a custom-defined service which executes the function appInitializerFn() during the application initialisation process (bootstrapping).

Step 4: If you have paid attention, you will have realised we need to define the appInitializerFn() function. Correct.

Let's do this somewhere near the top of the app.module.ts file (or core.module.ts file if you are using my multi-module starter from GitHub):

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

...

@NgModule({
    ...
})
export class AppModule {}

You may notice that the appInitializerFn() returns a promise rather than an Observable as is common in most Angular 5+ applications. That's because at the time of writing, the APP_INITIALIZER only supports Promises.

Step 5: We are done. Here's how you can use environment variables which are in the app-config.json file. Let's say you have a service which calls an endpoint on your API:

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/');
    }
}

or, similarly:

...
getNames() {
    return this.http.get(this.environment.config.servicesBasePath + '/names/');
}
...

If you run in to any issues, or what to discuss any of the content covered, I would love to hear from you. Please tweet me!