Dynamic Environment Variables in Angular

Posted on: 1/16/2022

Overview

Normally, Angular uses environment variables by importing them from environment.ts under environment folder. If you want to switch environment variables depending on the environment, you should define enviroment.dev.ts, enviroment.prod.ts, and so on for that environment, and define fileReplacements in angular.json. However, the problem here is that these environment variables are embedded in the build file when Angular is built, so you can't overwrite them later. For example, if the environment variables are different between the staging and production environments, Anuglar must be built separately in the staging and production environments, and the same artifact cannot be brought from the staging environment to the production environment.

This article is mainly about how to solve the above situation so that environment variables can be switched from outside. To summarize the conclusion at first, the following two points are important.

  • In main.ts, get the dynamic environment variables and rewrite envrionment.ts to Provide.
  • Environment variables are not referenced directly from files, but are injected from DI continer.

Implementation

Rewriting environment variables in main.ts

main.ts is a file that always exists under scr of an Angular project, and it is the starting point for launching Angular applications. Normally, this main.ts is executed, bootstrapping AppModule, and then AppModule bootstraps AppComponent, and so on, to launch the Angular application.

If you open main.ts, you can see that the main function is actually defined and executed, and that within the main function, platformBrowserDynamic() and the function bootstrapModule() following it are executed. The latter, bootstrapModule(), is a function to bootstrap the AppModule I mentioned earlier, but the former, platformBrowserDynamic(), can actually Provide for 'platform'.

In general, in Angular applications, the Injector of AppModule is the Root Injector, and above it is the Null Injector, but if you Provide the value with platformBrowserDynamic(), you can actually Provide to 'platform', which is one level above root. This time, we will rewrite the environment variables in main.ts (before starting AppModule), and then Provide the environment variables here. For more information about 'platform', please refer to the official document or the following video.

The code is as follows.

main.ts
import { enableProdMode, InjectionToken } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import { overrideEnv } from './override-env'; if (environment.production) { enableProdMode(); } /** * The definition of InjectionToken is written in main.ts for convenience, but I think it is better to put it in a separate file. */ export const ENVIRONMENT = new InjectionToken<Record<string, unknown>>( 'environment' ); const main = async () => { overrideEnv(environment).then((dynamicEnv) => { platformBrowserDynamic([{ provide: ENVIRONMENT, useValue: dynamicEnv }]) .bootstrapModule(AppModule) .catch((err) => console.error(err)); }); }; main();

The overrideEnv, which is executed first in the main function, should be implemented as follows.

override-env.ts
export const overrideEnv = async <T>(env: T): Promise<T> => { try { const response = await fetch('https://sample.com/api/client-env'); const dynamicEnv: T = await response.json(); for (const key in dynamicEnv) { if (Object.prototype.hasOwnProperty.call(dynamicEnv, key)) { const targetProp = env[key]; if (targetProp !== undefined) { env[key] = dynamicEnv[key]; } } } } catch (e) {} return env; };

This overrideEnv() takes an environment variable as an argument, rewrites the value of the environment variable retrieved from the server if there is a matching property, and returns the rewritten environment variable. The internal processing of the function (how to retrieve dynamic environment variables and how to merge the retrieved environment variables) needs to be implemented according to the circumstances of each project.

Using environment variables

The environment variable is now ready. In order to use this environment variable, DI using the InjectionToken that we defined earlier.

sample-component.ts
import { Component, Inject, OnInit } from '@angular/core'; import { ENVIRONMENT } from 'src/main'; @Component({ selector: 'app-sample', templateUrl: './sample.component.html', styleUrls: ['./sample.component.scss'], }) export class SampleComponent implements OnInit { constructor(@Inject(ENVIRONMENT) env: Record<string, unknown>) {} ngOnInit(): void {} handleClick(): void { console.log(this.env); } }

Of course, it is necessary to use the value of the environment variable provide to 'platform', so it is necessary to prohibit the direct reference to the environment variable from environment.ts. Although this is not the case in the example in this article, I think it is possible to specify @deprecated in environment.ts by TSDoc/JSDoc in the environment object itself.

Summary

In order to handle environment variables dynamically, I introduced how to override environment variables in main.ts and Provide. As a side note, this time I used main.ts to inject the process before the Angular application is launched, but Angular provides APP_INITIALIZER, an InjectionToken, which can be used to inject the process before the AppModule bootstraps the AppComponent a little later. However, Angular provides a InjectionToken called APP_INITIALIZER, which allows you to insert a process before the AppModule bootstraps the AppComponent. This is basically a mechanism for initializing the service, and is not suitable (and probably not possible) for proving values, but it can be useful to know. If you want to use Service to get environment variables, you can initialize the environment variable Service with APP_INITIALIZER.