When deploying our Angular applications usually we want to deploy it to multiple stages, for instance development, test, and production, and each stage has its own configuration values. In this article we'll explore three ways to load a different configuration for each stage, and review to see if they fulfill our requirements below.
- Use different configurations for deployment stages
- Change configuration values without rebuilding the solution
- Have configuration loaded before app initialization
Environment files
Using a new angular solution, we'll start by creating a new folder called environments
, inside it create two more files environment.ts
& environment.prod.ts
as in the snippet bellow.
If you used Angular CLI to generate the solution, the steps above should be already completed. The files we created are typescript files, so you can define variables of any kind, even functions, and then to use them just import the exported variables from this file.
Environment files work by replacing(beware: replacing the whole file, not overwriting the values) the default file(usually environment.ts
) with another file specific to the configuration from angular.json
. Since we used Angular CLI to generate the solution, we should have the file replacement settings preconfigured.
In the above snippet it is configured that for the configuration called production
, when compiling the file environment.ts
should be replaced with environment.prod.ts
. If we needed to use configuration values for one more stage except the two that we have already, we would create a new entry in the configurations
object in angular.json
, and replace the environment.ts
with the file containing configuration for that stage.
A final step would be to pass the desired configuration when building the application to apply its settings, to do so pass the configuration
parameter to the build command.
ng build --configuration=development/production/testing
Solution review: This is a simple and fast solution, we can define typescript variables and use them, one or more files for each configuration which replaces the files from the default configuration, pretty straightforward, and it covers most of the scenarios. The problem comes from the fact that we'll need to have a separated build for each stage, as the file replacement happens at compile time, since the variables inside these files need to be used in the compilation process.
Only 2/3 requirements fulfilled
✔ Use different configurations for deployment stages
❌ Change configuration values without rebuilding the solution
✔ Have configuration loaded before app initialization
APP_INITIALIZER way
Our previous solution did the job done, but it required a different build for each stage. To decouple the configuration data from our build we would need to load it from an external source, like a JSON file.
To not go very far, we can load our configuration file from assets, we'll need to do a GET request to load it. Lets create a service environment-config.service.ts
which does the request, loads the file and then exposes the configuration data.
The next step would be to call the service loadConfiguration
function and load data from our JSON file. Since the data is loaded via a GET request, this is an asynchronous operation, and we need to makes sure that it is loaded before using it.
To achieve that we can leverage the power of APP_INITIALIZER
, which lets us provide an initialization function which is executed during app initialization, and if that function returns a Promise or an Observable, initialization does not complete until the Promise is resolved or the Observable is completed.
Now the configuration data is accessible everywhere the service can be injected via the getConfig
function. To have different configuration data based on the stage we need to make sure that the proper file is used in your deployment pipeline.
Solution review:
Using this solution we can build the project only once, and use the artifact for multiple stages, the configuration will be loaded from an external source (our case assets
folder, but can be from anywhere). The initialization won't finish till the file is not loaded so the application will always have access to configuration data.
The problem with this solution is that if you need the configuration data to be available during app initialization process (either to use it in a Module intialization or with another APP_INITIALIZER function), there is no way to make sure that the configuration is loaded first, and your code might try to access this data before it is loaded.
Again only 2/3 requirements fulfilled, at this point it starts to feel like a trilemma :)
✔ Use different configurations for deployment stages
✔ Change configuration values without rebuilding the solution
❌ Have configuration loaded before app initialization
Fetch way
To have access to configuration data during app initialization, that data needs to be loaded before app initialization. We could do so by loading the JSON file with a fetch
call, and then to provide the result via the DI mechanism of Angular.
Start by creating a new function called loadRuntimeConfiguration
in a new file load-configuration.ts
, this function receives the file path and returns its content.
We've also defined an InjectionToken
called EnvironmentConfigInjector
, used to provide the configuration data object via the DI mechanism of Angular.
Next step would be to call fetch before app initialization, well, that is before calling bootstrapModule
being called in main.ts
. So lets call loadRuntimeConfiguration
, use its result to provide the configuration via DI using the EnvironmentConfigInjector
InjectionToken, the platformBrowserDynamic
accepts a StaticProvider[]
parameter where we can register our token.
Now that we have the configuration data registered in a provider, we can use it everywhere Angular's DI has access to. ex:
Solution review:
This solution might seem a bit hacky, since usually we don't touch main.ts
, but since platformBrowserDynamic
has support for extra providers we can leverage that to achieve our needs. One remark is that the app initialization process is delayed till configuration is being loaded, but we need that data before app initialization, there is not other way I know of.
Not a trilemma anymore, 3/3:
✔ Use different configurations for deployment stages
✔ Change configuration values without rebuilding the solution
✔ Have configuration loaded before app initialization
I am open to improve upon the solutions above, if you have any suggestions feel welcome to leave a comment.