March 1st, 2018

Angular How-to: Editable Config Files

Developer Support
Cloud Solution Architects

In this post, Premier Developer consultant Laurie Atkinson walks through how to allow editing of your Angular configuration files after your app has been built, bundled, and deployed.

*This post was updated on 12/3/2018 to reflect the latest changes to Angular.


The Angular-CLI is the recommended way to build a production-ready app, complete with bundling, uglifying, and tree-shaking. An Angular-CLI generated application even comes with a mechanism for creating environment-specific versions. However, those configuration files are in TypeScript and do not allow editing by IT staff or automated deployment tools such as VSTS. This post provides the steps and code samples for using a JSON configuration file, which can be customized for multiple environments.

Define TypeScript interface for config settings

The use of interfaces in an Angular app provides intellisense and type-safety for your entities. For this example, refer to this sample configuration file.

app-config.model.ts

export interface IAppConfig {
    env: {
        name: string;
    };
    appInsights: {
        instrumentationKey: string;
    };
    logging: {
        console: boolean;
        appInsights: boolean;
    };
    aad: {
        requireAuth: boolean;
        tenant: string;
        clientId: string;

    };
    apiServer: {
        metadata: string;
        rules: string;
    };
}

Create JSON config files

A convenient place to store configuration files is under the assets folder of your project. Using the interface defined above, sample files could look as follows:

assets\config\config.dev.json

{
    "env": {
    "name": "DEV"
     },
    "appInsights": {
    "instrumentationKey": "<dev-guid-here>"
     },
    "logging": {
    "console": true,
    "appInsights": false
    },
    "aad": {
    "requireAuth": true,
    "tenant": "<dev-guid-here>",
    "clientId": "<dev-guid-here>"
    },
    "apiServer": {
    "metadata": "https://metadata.demo.com/api/v1.0/",
    "rules": "https://rules.demo.com/api/v1.0/"
    }
}

assets\config\config.deploy.json (Note placeholders that are replaced during deployment)

{
    "env": {
    "name": "#{envName}"
    },
    "appInsights": {
    "instrumentationKey": "#{appInsightsKey}"
    },
    "logging": {
    "console": true,
    "appInsights": true
    },
    "aad": {
    "requireAuth": true,
    "tenant": "#{aadTenant}",
    "clientId": "#{aadClientId}"
    },
    "apiServer": {
    "metadata": "https://#{apiServerPrefix}.demo.com/api/v1.0/",
    "rule": "https://#{apiServerPrefix}.demo.com/api/v1.0/",
    }
}

Continue to use environment.ts with Angular-CLI build

The Angular-CLI creates several TypeScript environment files in the environments folder. They will still be used, but contain only the environment name.

environments\environment.dev.json

export const environment = {
    name: 'dev'
};

environments\environment.deploy.json

export const environment = {
    name: 'deploy'
};

angular.json

"projects": {
  "my-app": {
    "architect": {
      "build": {
        "configurations": {
          "deploy": {
            "fileReplacements": [
              {
                "replace": "src/environments/environment.ts",
                "with": "src/environments/environment.deploy.ts"
              }
            ],
            . . .
          }
        }
      },
      "serve": {
        . . .
        "configurations": {
          "deploy": {
            "browserTarget": "my-app:build:deploy"
          }

Create a service to read config file

This service will read the correct config file and store the result in a static field in this class..

app.config.ts (Note the use of the interface defined above and config file naming convention to retrieve the appropriate file.)

import { Injectable } from '@angular/core’;
import { HttpClient } from '@angular/common/http';
import { environment } from '../environments/environment';
import { IAppConfig } from './models/app-config.model';
@Injectable()
export class AppConfig {
    static settings: IAppConfig;
    constructor(private http: HttpClient) {}
    load() {
        const jsonFile = `assets/config/config.${environment.name}.json`;
        return new Promise<void>((resolve, reject) => {
            this.http.get(jsonFile).toPromise().then((response : IAppConfig) => {
               AppConfig.settings = <IAppConfig>response;
               resolve();
            }).catch((response: any) => {
               reject(`Could not load file '${jsonFile}': ${JSON.stringify(response)}`);
            });
        });
    }
}

Load config file prior to app creation

Angular includes a token named APP_INITIALIZER that allows our app to execute code when the application is initialized. In the app module, use this token to invoke the load method in our config service. Since our method returns a promise, Angular will delay the initialization until the promise is resolved.

app.module.ts

import { APP_INITIALIZER } from '@angular/core';
import { AppConfig } from './app.config';

export function initializeApp(appConfig: AppConfig) {
  return () => appConfig.load();
}
@NgModule({
    imports: [ , , , ],
    declarations: [ . . . ],
    providers: [
       AppConfig,
       { provide: APP_INITIALIZER,
         useFactory: initializeApp,
         deps: [AppConfig], multi: true }
    ],
    bootstrap: [
      AppComponent
    ]
})
export class AppModule { }

Consume the app settings throughout the application

The config settings are now available from anywhere in the application and they include type-checking provided by the interface.

export class DataService {
    protected apiServer = AppConfig.settings.apiServer;
    . . .
    if (AppConfig.settings.aad.requireAuth) { . . . }
}
export class LoggingService {
    . . .
    instrumentationKey: AppConfig.settings && AppConfig.settings.appInsights ?
                        AppConfig.settings.appInsights.instrumentationKey : ''
    . . .
    if (AppConfig.settings && AppConfig.settings.logging) { . . . }
}

Note: to build a production version of the app using an environment name other than prod, use this command:

ng build –configuration=deploy

(Link to Portuguese version of this article)

Author

Developer Support
Cloud Solution Architects

Microsoft Developer Support helps software developers rapidly build and deploy quality applications for Microsoft platforms.

15 comments

Discussion is closed. Login to edit/delete existing comments.

  • Miroslav Hristov

    Hi Laurie Atkinson,

    I like your approach and we are using it in this specific scenario. I have a comment about naming - you are using class AppConfig - which indeed is provider of configuration settings and meanwhile - you have interface with name- IAppConfig. When I saw it for the first time I expected that this interface is for "describing" this class - its properties/functions. I agree and I like this approach to expose an...

    Read more
  • Harry Hathorn [OnePointFour Consulting]

    why don’t you just save config straight in the environments files? I don’t see a point to this whole app.config thing.

    • Todd Davis

      Because the environment files are read at build-time, not at run-time.

      If, for example, you needed to move the API server to a new IP address, in order to do that using the environment files, you'd have to make the change and then recompile and redeploy the whole app for the change to take place.

      Using this method, a simple file change is all that is needed, and then restart the web app. No need...

      Read more
  • Dan Chase

    I've been using this method for some time successfully.
    I ended up creating another file (suggestion from a colleague) that had some constants, such as BASE_URL etc.. then I split into two files, one api-calls.json which had actual API calls, and another for general environment-specific app configuration (such as BASE_URL).

    app-config.json:
    [
    Profile: "Local",
    Local: { BASE_URL: "http://localhost:53324/api" },
    Test : { BASE_URL: "http://test.mydomain.com/api" },
    Prod: {...

    Read more
  • Daniel Codrea

    I added this on Angular 6
    I am trying to run this solution, but it doesen’t work. I tried a number of these kind of solutions and every each of them fails, because the response data after the promise is never defined (message: “response is not defined”).

  • Sam Bassett

    "Note: to build a production version of the app using an environment name other than prod, use this command:
    ng build –configuration=deploy"Wait, so i cant change the config file without re building the app?  Then its not really a config file at all if its baked into the app at build time
    This is a bad strategy and a bad pattern. A good solution would be to build the app once, and have that one build...

    Read more
    • Daniel Codrea

      I have the same needs. Did you find any solutions to reload configs without rebuilding the app?

    • AdDubs

      I think you are missing the point in that at deploy time this allows to add the correct configuration to any environment that you will be deploying to using some form of release variables.
      It is very useful when say, deploying the same code to mutiple different Tenants (companies/enterprises). You can have release variables set up in your release definition for each environment t(tenant) you are deploying to. And all you need to do is...

      Read more
      • Craig Stanton

        Our use case mightbe simalr to Sam's. We want one build to be submitted to the app store and then allow the testers to reconfigure it to use only the development servers so they can submit test data without polluting production. This could be done through some secret key stroke or tap sequence but could also be triggered by the appeararance of another file on the device. e.g if debug.txt is found in the documents...

        Read more
      • Daniel Codrea

        I think you are missing the point.
        Here are some arguments on why you should avoid one build per environment:
        - It slows down CI pipelines because it needs to create a build for every environment
        - It increases the risk of errors/differences in different environments because the builds are separate
        - It adds unnecessary information about other environments in the code
        - You might, for security reasons, don’t want to show confidential information in...

        Read more
  • Ben C

    Hi, I've been incorporating this functionality into my Angular 6 web app and have noticed that my config settings are not actually available anywhere in the application as mentioned. Specifically, they don't seem to be available in my injectable classes unless I call it within a class method. So for example, in something like this where ConfigService is the name of the config file read service:
     
    @Injectable()
    export class TestService {
        endpoint...

    Read more
    • Daniel Codrea

      I have the exact same problem. Is there nobody adressing this?

    • Alex Jamrozek

      I have similar experiences.

  • Guzman Tenorio, Nuria

    Hi guys,
    nice tutorial, thanks!
    One question, where do you decide that assets\config file will used?  I don’t know where you determine the values to replaces the placeholders in assets\config\config.deploy.json
    Regards