Using env.js files

When we deploy our Angular application, chances are big that we will let our data-access layer connect to another api url than the one we connect to in development. This means that this api url should be configurable at some point. There are multiple ways of working with environment variables in Angular code-bases. I like to choose for a simple env.js file that can easily be replaced by the continuous integration (read this article). This way the env.js file is never really part of the build and can be replaced at all times. This file could look like this:

// app-name/src/env.js
(function (window) {
  window.__env = {};
  window.__env.apiUrl = 'http://localhost:1234/api';
})(this);

As we can see above we use an immediately invoked function expression (iife) where we just set these environment variables directly on the window object. This approach would not work for server-side rendered applications but that’s beyond the goal of this article.

How do we achieve a working solution?

What we need to do in the angular.json file is to add this env.js file to the options.assets array of the build target, so it will be available after compilation time. We also need to import it into the index.html of our application.

  <head>
    <!-- Load environment variables -->
    <script src="env.js"></script>
  </head>
  ...

What we did so far is we made sure the variable apiUrl will be available in the __env property of the window object which is available everywhere in the frontend. Calling the window directly in Angular is seen as a bad practice and calling window['__env'].apiUrl everywhere is even worse.

To consume the apiUrl in our Angular application in a proper way, we need to create an InjectionToken that we can use for dependency injection. Let’s go ahead and create an injection-tokens.ts file that exposes the API_URL token.

// app-name/src/injection-tokens.ts
import { InjectionToken } from "@angular/core";

export const API_URL = new InjectionToken<string>('API_URL');

We have our environment variables living on our window object and we have an injection token that we will use to inject that in any constructor. The last thing that we need to do tell Angular that injecting the API_URL token into a constructor should retrieve the apiUrl living on the __env property of the window object. To do that we have to add some logic to the providers property of the @NgModule() decorator of our AppModule. We provide the API_URL in dthe provide property, we use a factory where Document is injected and we use document.defaultView which refers to the window object to retrieve the apiUrl living in the __env property. Since we inject Document we need to add DOCUMENT to the deps property for it to become available.

@NgModule({
  ...
  providers: [
    {
      provide: API_URL,
      useFactory: (document: Document) => {
        return document.defaultView['__env'].apiUrl;
      },
      deps: [DOCUMENT],
    },
  ]
})
export class AppModule {}

This is great! Now in our services we can just inject the API_URL with the @Inject() decorator, and our service knows exactly where to send its XHR calls to.

import { Injectable, Inject } from '@angular/core';
import { CONDITIONAL_API_URL } from './injection-tokens';
@Injectable({providedIn:'root'})
export class FooService {
  constructor(@Inject(API_URL) private readonly apiUrl) {
  }
  ...
}

Extra problem

The essence of this article is not to explain environment variables nor InjectionTokens. Let’s dive a bit deeper than that.

A short while ago a client of mine had this specific use-case: They had an entire application that was already running in production. When their product grew they realised that there was a use-case where they wanted that exact application (the exact same logic) but they needed it to connect to another apiUrl.

The first thing that would come to mind, is to change the value of apiUrl in our env.js but the thing was they also needed the previous flow to keep on working. They actually needed two flows of the same application that both had their own endpoint. Flow one needed to consume the old apiUrl and flow two needed to consume the other apiUrl.

Let’s try to implement this:

Since we don’t want to touch the current routing flow we will pass a QueryParam called secondary=true to the url of our application. If that QueryParam exists we don’t want to consume window['__env'].apiUrl but window['__env'].secondaryApiUrl and if it doesn’t exist we want to use window['__env'].apiUrl.

Let’s add the secondaryApiUrl to the env.js file and update the injection-tokens.ts and app.module.ts accordingly.

// app-name/src/env.js
(function (window) {
  window.__env = {};
  window.__env.apiUrl = 'http://localhost:1234/api';
  window.__env.secondaryApiUrl = 'http://localhost:4321/api';
})(this);
// app-name/src/injection-tokens.ts
import { InjectionToken } from "@angular/core";

export const API_URL = new InjectionToken<string>('API_URL');
export const SECONDARY_API_URL = new InjectionToken<string>('SECONDARY_API_URL');
// app-name/src/app.module.ts
@NgModule({
  ...
  providers: [
    {
      provide: API_URL,
      useFactory: (document: Document) => {
        return document.defaultView['__env'].apiUrl;
      },
      deps: [DOCUMENT],
    },
    {
      provide: SECONDARY_API_URL,
      useFactory: (document: Document) => {
        return document.defaultView['__env'].secondaryApiUrl;
      },
      deps: [DOCUMENT],
    },
  ]
})
export class AppModule {}

How will we decide which apiUrl to take?

In our FooService we could inject both the API_URL and the SECONDARY_API_URL together with the ActivatedRoute but that seems like a lot of redundant code to implement in every service.

@Injectable({providedIn:'root'})
export class FooService {
  constructor(
    private readonly activatedRoute: ActivatedRoute,
    @Inject(API_URL) private readonly apiUrl,
    @Inject(SECONDARY_API_URL) private readonly secondaryApiUrl,

    ){
      const conditionalApiUrl = activatedRoute.snapshot.queryParams.secondary
      ? secondaryApiUrl
      : apiUrl;
  }
}

The following code would be way nicer and that’s what we will implement in a minute:

import { Injectable, Inject } from '@angular/core';
import { CONDITIONAL_API_URL } from './injection-tokens';

@Injectable({providedIn:'root'})
export class FooService {

 constructor(@Inject(CONDITIONAL_API_URL) private readonly apiUrl) {
   // console.log(apiUrl);
  }
}

For this to work we need to create a new InjectionToken called CONDITIONAL_API_URL that will determine which api url to choose. CONDITIONAL_API_URL is just a token that will use the inject function of Angular to inject our 3 dependencies and will decide which api url to use based on the existence of the secondary QueryParam.

import { inject, InjectionToken } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

export const API_URL = new InjectionToken<string>('API_URL');
export const SECONDARY_API_URL = new InjectionToken<string>(
  'SECONDARY_API_URL'
);
export const CONDITIONAL_API_URL = new InjectionToken('CONDITIONAL_API_URL', {
  factory() {
    const activatedRoute = inject(ActivatedRoute);
    const apiUrl = inject(API_URL);
    const secondaryApiUrl = inject(SECONDARY_API_URL);
    return activatedRoute.snapshot.queryParams.secondary
      ? secondaryApiUrl
      : apiUrl;
  },
});

There is no need to provide CONDITIONAL_API_URL, we can just inject it in the FooService as we just shown. Every time the application is loaded with the secondary queryParam in the url it will use the SECONDARY_API_URL. Otherwise it will use the API_URL;

Conclusion

InjectionTokens are a powerful thing and combining them with the inject() function of Angular (service locator pattern) we can combine tokens and put functionality in them. For more information on the inject() function of Angular, take a look at this article.

Here is a stackblitz example where we can find the source code. Note: external javascript files in those stackblitz projects are not possible at the time of writing this. That’s why we have added the contents of the env.js file directly into the index.html.

Thanks for the reviewers

Thanks, Wim Holvoet for the idea of using injection tokens.