Application configuration on Angular. Best practics

How to manage environment configuration files and goals



When you created an angular application using the Angular CLI or Nrwl Nx tools , you always have a folder with environment configuration files:







<APP_FOLDER>/src/environments/ └──environment.ts └──environment.prod.ts
      
      





You can rename environment.prod.ts to environment.production.ts for example, you can also create additional configuration files such as environment.qa.ts or environment.staging.ts .







 <APP_FOLDER>/src/environments/ └──environment.ts └──environment.prod.ts └──environment.qa.ts └──environment.staging.ts
      
      





The environment.ts file is used by default. To use the remaining files, you must open angular.json and configure the fileReplacements section in the build configuration and add blocks to the serve and e2e configurations.







 { "architect":{ "build":{ "configurations":{ "production":{ "fileReplacements":[ { "replace":"<APP_FOLDER>/src/environments/environment.ts", "with":"<APP_FOLDER>/src/environments/environment.production.ts" } ] }, "staging":{ "fileReplacements":[ { "replace":"<APP_FOLDER>/src/environments/environment.ts", "with":"<APP_FOLDER>/src/environments/environment.staging.ts" } ] } } }, "serve":{ "configurations":{ "production":{ "browserTarget":"app-name:build:production" }, "staging":{ "browserTarget":"app-name:build:staging" } } }, "e2e":{ "configurations":{ "production":{ "browserTarget":"app-name:serve:production" }, "staging":{ "browserTarget":"app-name:serve:staging" } } } } }
      
      





To build or run an application with a specific environment, use the commands:







 ng build --configuration=staging ng start --configuration=staging ng e2e --configuration=staging  ng build --prod     ng build --configuration=production
      
      





Create an interface for environment files



 // environment-interface.ts export interface EnvironmentInterface { production: boolean; apiUrl: string; } // environment.ts export const environment: EnvironmentInterface = { production: false, apiUrl: 'https://api.url', };
      
      





Do not use environment files directly, only through DI



Using global variables and direct imports violates the OOP approach and complicates the testability of your classes. Therefore, it is better to create a service that can be injected into your components and other services. Here is an example of such a service with the ability to specify a default value.







 export const ENVIRONMENT = new InjectionToken<{ [key: string]: any }>('environment'); @Injectable({ providedIn: 'root', }) export class EnvironmentService { private readonly environment: any; // We need @Optional to be able start app without providing environment file constructor(@Optional() @Inject(ENVIRONMENT) environment: any) { this.environment = environment !== null ? environment : {}; } getValue(key: string, defaultValue?: any): any { return this.environment[key] || defaultValue; } } @NgModule({ imports: [ BrowserModule, HttpClientModule, AppRoutingModule, ], declarations: [ AppComponent, ], // We declare environment as provider to be able to easy test our service providers: [{ provide: ENVIRONMENT, useValue: environment }], bootstrap: [AppComponent], }) export class AppModule { }
      
      





Separate your environment configuration and business logic



The environment configuration includes only properties that relate to the environment, for example apiUrl . Ideally, the environment configuration should consist of two properties:







 export const environment = { production: true, apiUrl: 'https://api.url', };
      
      





Also in this config you can add a property to enable debugMode: true debug mode or you can add the name of the server where the environmentName: 'QA' application is running, but do not forget that this is very bad practice if your code knows anything about the server on which it is running .







Never store any sensitive information or passwords in an environment configuration.







Other configuration settings such as maxItemsOnPage or galleryAnimationSpeed should be stored in another place and it is advisable to use configuration.service.ts, which can receive settings from some endpoint or simply load config.json from the assets folder.







1. Asynchronous approach (use when the configuration may change in runtime)



 // assets/config.json { "galleryAnimationSpeed": 5000 } // configuration.service.ts // ------------------------------------------------------ @Injectable({ providedIn: 'root', }) export class ConfigurationService { private configurationSubject = new ReplaySubject<any>(1); constructor(private httpClient: HttpClient) { this.load(); } // method can be used to refresh configuration load(): void { this.httpClient.get('/assets/config.json') .pipe( catchError(() => of(null)), filter(Boolean), ) .subscribe((configuration: any) => this.configurationSubject.next(configuration)); } getValue(key: string, defaultValue?: any): Observable<any> { return this.configurationSubject .pipe( map((configuration: any) => configuration[key] || defaultValue), ); } } // app.component.ts // ------------------------------------------------------ @Component({ selector: 'app-root', changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { galleryAnimationSpeed$: Observable<number>; constructor(private configurationService: ConfigurationService) { this.galleryAnimationSpeed$ = this.configurationService.getValue('galleryAnimationSpeed', 3000); interval(10000).subscribe(() => this.configurationService.load()); } }
      
      





2. Synchronous approach (use when the configuration almost never changes)



 // assets/config.json { "galleryAnimationSpeed": 5000 } // configuration.service.ts // ------------------------------------------------------ @Injectable({ providedIn: 'root', }) export class ConfigurationService { private configuration = {}; constructor(private httpClient: HttpClient) { } load(): Observable<void> { return this.httpClient.get('/assets/config.json') .pipe( tap((configuration: any) => this.configuration = configuration), mapTo(undefined), ); } getValue(key: string, defaultValue?: any): any { return this.configuration[key] || defaultValue; } } // app.module.ts // ------------------------------------------------------ export function initApp(configurationService: ConfigurationService) { return () => configurationService.load().toPromise(); } @NgModule({ imports: [ BrowserModule, HttpClientModule, AppRoutingModule, ], declarations: [ AppComponent, ], providers: [ { provide: APP_INITIALIZER, useFactory: initApp, multi: true, deps: [ConfigurationService] } ], bootstrap: [AppComponent], }) export class AppModule { } // app.component.ts // ------------------------------------------------------ @Component({ selector: 'app-root', changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { galleryAnimationSpeed: number; constructor(private configurationService: ConfigurationService) { this.galleryAnimationSpeed = this.configurationService.getValue('galleryAnimationSpeed', 3000); } }
      
      





Replace environment variables during deployment or runtime



Many teams violate the rule “Build once, deploy many” by assembling the application for each environment instead of just replacing the configuration in the already built build.







Do not create separate assemblies with different configurations; instead, use only one production assembly and substitute values ​​during the deployment or during code execution. There are several options for doing this:







Replace the values ​​with placeholders in the environment files that will be replaced in the final assembly during the deployment



 export const environment = { production: true, apiUrl: 'APPLICATION_API_URL', };
      
      





During the deployment, the line APPLICATION_API_URL should be replaced with the real address of the api server.







Use global variables and inject config files with docker volumes



 export const environment = { production: true, apiUrl: window.APPLICATION_API_URL, }; // in index.html before angular app bundles <script src="environment.js"></script>
      
      





Thank you for your attention to the article, I will be glad to constructive criticism and comments.










Also join our community on Medium , Telegram or Twitter .








All Articles