The best way to use Angular’s environment files

An introduction to environment files

Environment files in Angular, when using the CLI, are a great way to add per-environment configuration to your app. You can have your app use different runtime values for dev, stage and production environments without having to hardcode any environment information, or conditional logic to switch between environments, into your app.

Note: If you haven’t been taking advantage of the ‘environments’ feature of the Angular CLI you should probably check it out.

Here’s some links to blogs and documentation explaining what it is and how to use it at a basic level.

When you create a new Angular project using the CLI you are provided with an environments folder with (2) environment files, one for production and one for all other environments.

Your first impulse is to find a way to use environment.ts to your advantage, shoving all sorts of things inside it — api keys, feature flags, api urls, default configuration settings, your grandmother’s birthday…

Pretty soon that environment.ts file is going to be packed with all sorts of state (the fact that it typically doesn’t change during runtime doesn’t mean it’s not state) that your services, directives and components will depend on.

And since Angular swaps in the correct version of the file when you do a build with an environment flag on the command line, you can sleep well at night knowing the values you use for dev stay in dev and the values you use in production are only used in production!

Except there is a problem here…

[your] environment.ts file is going to be packed with all sorts of state … that your services, directives and components will depend on

Testability

One of the benefits of using Angular as a framework is the focus on testability.

Angular provides a Dependency Injection system which provides seams in your application where you can swap out one implementation of a dependency for another.

Angular provides abstractions of the runtime and browser apis via services like the Render and ElementRef so we don’t have interact directly with the DOM and can run our code in other environments like the server for server-side rendering or automated testing.

But every time you add the line import { environment } from '../environments/environment' (your import paths my vary) you are taking a dependency on a non-testable external dependency, which in our case is a file on the file system.

The environment file is not part of Angular’s injector graph and so it can’t be replaced with something different when testing.

While the Angular CLI build toolchain does swap the file bundled into your app code when you run ng serve or ng build , this is not something you can change at test-runtime because it’s already set by the time your tests start to run.

The goal of unit testing is to isolate parts of your code from external dependencies and construct scenarios for those parts to act under. You then see if the results of executing your code matches your expectations for those controlled scenarios.

We need to be able to isolate our code from environment configuration…

… every time you add the line import { environment } from '../environments/environment' … you are taking a dependency on a non-testable external dependency

Creating an abstraction

One of the best ways to separate parts of a system is to create an abstraction between them — adding a layer, if you will.

We can use this strategy to fix our environment dependency problem.

Let’s start by fixing up our /environments folder.

One pattern I use in all my Angular CLI apps is to create an environment interface ienvironment.ts . This file helps keep all my different environment files consistent with each other.

ienvironment.ts with typings for our other environment files

Now implement this interface in all your various environment files. This will ensure that when you update properties in your interface all the environment files will need to add these properties to avoid compile-time errors (and therefore runtime errors).

environment.ts typed by our environment interface

Now that we have a well structured /environments folder let’s create our next abstraction

We need a way to integrate environment.ts into Angular’s DI system so that we can substitute it at test time with a mockable/spy-able dependency.

We could use an InjectionToken which would allow us to supply a value as an injectable token to our classes but this pattern works much better in a library and the syntax isn’t very ergonomic — you don’t want to wrap a commonly used constructor parameter in @Inject(...) if you can help it.

Instead let’s use a service, environment.service.ts

Since we have that interface we created for our environment files, let’s put that to good use too and implement it on our service.

environment.service.ts guards access to our environment files and abstracts away this dependency

Now we add our service as a dependency of the app.component.ts and assign a value from our service to the component class instance.

app.component.ts using our environment.service.ts to use values coming from our environment configuration

Finally let’s write a test that uses a mock version of the service to set our environment values. Since our service is just a pass-through of our environment file there’s definitely no behavior or logic to fake, which is a normal reason to replace a dependency, but we are removing a dependency on hardcoded values on the filesystem.

app.component.spec.ts in which we can mock our environment configuration — our test is no longer reliant on any external dependencies

[with our environment.service.ts] we are removing a dependency on hardcoded values on the filesystem.

One bonus here is that our original environment import was a plain javascript object and Typescript knew it had no write protection so there was nothing stopping you from accidentally coding this somewhere in your app…

import { environment } from '@environments/environment';...someMethod() {
this.myField = environment.apiUrl;
// ... make an api request // OOPS !!!!!
environment.apiUrl = this.someOtherField;
// ... make an api request that is sure to fail
}

But now our environment.service.ts exposes readonly values, not a mutable object, so something like this will have the Typescript compiler complaining about an error.

// [ts] Cannot assign to 'apiUrl' because it is a constant
// or a read-only property.
environment.apiUrl = 'test';

Isn’t that nice!

Be conscious of your enviroment(s)

As you can see, adding a layer of abstraction between our external, non-test friendly environment.ts dependency and our application components/services means we have an opportunity to:

  1. Insert a mock or use the original values (we choose!)
  2. Set up different scenarios of our choosing during testing by crafting specific states (simulate a bad config file?)
  3. Redefine the rules for accessing our environment.ts data using Typescript (avoid confusing mistakes :D )

So what’s the best way to use Angular’s environment files??

Sparingly and behind a layer of abstraction! But go ahead and use that abstraction to your heart’s content!

I hope you’ll keep these suggestions in mind the next time you find yourself beginning to type import { environment } from ...

Below is a link to the source code

https://github.com/sgwatgit/demo-ng-environment

Next time I’d like to cover the difference between dynamic and static application configuration and how we can take advantage of both.

Let me know if this was helpful.

Thanks!

Web developer passionate about my craft and helping others grow in theirs.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store