Angular How-to: Editable Config Files

Developer Support

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)

15 comments

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

  • Guzman Tenorio, Nuria 0

    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

  • Ben C 0

    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 = ConfigService.settings.apiEndpoints.securityApiEndpoint;
     
        testMethod(testParam: string) {
            const endpoint2 = ConfigService.settings.apiEndpoints.securityApiEndpoint;
        }
    }
     
    The endpoint line will return an error because ConfigService.settings is null. But endpoint2 works just fine. However, if I’m working in a non-injectable component class, then endpoint will work just fine. Is this expected behavior? And if so, why is that? Is it because the config service is also injectable?

    • Alex Jamrozek 0

      I have similar experiences.

    • Daniel Codrea 0

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

  • Sam Bassett 0

    “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 that you can deploy on any environment with an External config file that it reads.When i have environments such as Dev, QA, Staging, sandbox, integration, Production.  It is a huge pain for devops and an anti-pattern to take one codebase and re-build the app 7 times with 7 different commands.I should be able to build it once, deploy it anywhere I want, and edit the config along side or after the deployment.

    • AdDubs 0

      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 click a button once your build and release definition is set up and configured for each environment.
      It sounds like you are more interested in making config changes at runtime. Well I’ll tell you this… That’s more of a bad practice than this. Next time you release your app your “on-the-fly” config updates will be overwritten unless you update the config in your source control. The fact that you prefer making config changes at runtime tells me that you don’t have a scripted deployment process (also bad practice)… Or might be encountering lots of bugs in your code / uncertainty of what your configuration values are supposed to be set to.

      • Daniel Codrea 0

        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 the environment config, which is saved to the version control

      • Craig Stanton 0

        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 directory then switch to debug mode. I don’t think this is particiularly rare.

    • Daniel Codrea 0

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

  • Daniel Codrea 0

    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”).

  • Dan Chase 0

    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: { BASE_URL: “http://prod.mydomain.com/api” },
    ]

    api-calls.json:
    [ { NAME: “URL_GetNameById”,
    URL: “{BASE_URL}/GetNameById?id={id}”
    ]

    In the subscribe event I use:
    appConfig.getApiCall(“URL_GetNameById”).Replace(“{id}”, record.Id)).subscribe();

    the getApiCall method replaces the BASE_URL for me so I never have to worry about it.

    Then while deploying just make sure I don’t copy over that file, ever, but update api-calls.json when needed.

    As for the arguments for/against using Angular built-in environments, the reason I didn’t use that was because we want to be able to move the distribution between servers without having to re-build it. After all, once you re-build it’s no longer tested, and there’s no good way to know what is where without trying to keep track of the hashes, or going back to reference commits, something programmer/analysts and infrastructure engineers would not be able to do without access to source control/etc.

  • Harry Hathorn [OnePointFour Consulting] 0

    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 0

      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 to recompile anything.

  • Miroslav Hristov 0

    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 interface for “describing” configuration settings. But in this case I would name it something like – “IAppConfigSettings” and respectively I would name class “AppConfig” to “AppConfigSettingsService” or “AppConfigProviderService”.

    PS: Thanks for the great article – it is very helpful for us.

Feedback usabilla icon