Using env.js files
When shipping an Angular application, it’s highly likely that the data-access layer will need to point to a distinct api url in production compared to development. Consequently, that api url must be adjustable at a certain moment. Various strategies exist for handling environment variables within Angular projects. A straightforward approach I favor is relying on a simple env.js file, which the continuous integration can readily swap out (read this article). By doing so, the env.js file remains outside the build itself, allowing for replacement at any time. An example of such a file is shown below:
// app-name/src/env.js
(function (window) {
window.__env = {};
window.__env.apiUrl = 'http://localhost:1234/api';
})(this);
The above snippet relies on an immediately invoked function expression (iife) that assigns these environment variables straight onto the window object. For server-side rendering scenarios, this method won't hold up, but that's outside the scope we're covering here.
What steps lead to a functional setup?
The trick lies in modifying the angular.json: drop this env.js into the options.assets list of the build target, ensuring it gets picked up when the compilation finishes. On top of that, we have to pull it into our app's index.html.
<head>
<!-- Load environment variables -->
<script src="env.js"></script>
</head>
...
Up to this point, we have ensured that the apiUrl variable is attached to the __env property of the window object, making it accessible globally in the frontend.
However, directly referencing window in Angular is considered a bad practice, and scattering window['__env'].apiUrl across the codebase is even less desirable.
To properly consume the apiUrl within our Angular app, we need to establish an InjectionToken that can be leveraged for dependency injection.
Let’s proceed by creating an injection-tokens.ts file that exports the API_URL token.
// app-name/src/injection-tokens.ts
import { InjectionToken } from "@angular/core";
export const API_URL = new InjectionToken<string>('API_URL');
Our environment variables are stored on the window object, and the injection token is ready for use in any constructor. The remaining step is to configure Angular so that when API_URL is injected, it pulls the apiUrl from the __env property of the window object. This is accomplished by adding a provider to the providers array in the @NgModule() decorator of AppModule. Inside the provider, we set up API_URL with a factory function that injects Document. The factory reads document.defaultView—which points to the window object—to access its __env property and return the apiUrl inside it. Because the factory relies on Document, we include DOCUMENT in the deps array to make that dependency available during injection.
@NgModule({
...
providers: [
{
provide: API_URL,
useFactory: (document: Document) => {
return document.defaultView['__env'].apiUrl;
},
deps: [DOCUMENT],
},
]
})
export class AppModule {}
Excellent — from this point on, any service can simply pull in API_URL via the @Inject() decorator, and it will automatically know the correct endpoint for its XHR requests.
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
This article isn’t really about environment variables or InjectionTokens. There’s a more interesting angle to explore.
Not long ago, one of my clients hit a particular situation:
They had a full application already live in production.
As their product evolved, they found a scenario where they wanted to reuse that same application — with the exact identical logic — but have it talk to a different apiUrl.
The obvious first move would be to swap the value of apiUrl inside env.js. However, the catch was that the existing flow still had to function. What they truly needed was two parallel flows of the same app, each hitting its distinct endpoint. The first flow would keep using the original apiUrl, while the second flow would direct its requests to the alternate one.
Let’s try to implement this:
To avoid interfering with the current routing logic, we’ll attach a QueryParam called secondary=true to the app’s URL. When this QueryParam is present, we bypass window['__env'].apiUrl and switch to window['__env'].secondaryApiUrl. In its absence, we fall back to window['__env'].apiUrl.
Now, we’ll add the secondaryApiUrl into the env.js file and modify the injection-tokens.ts and app.module.ts to match.
// 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 {}
What determines the selected apiUrl?
Within FooService, one option is to inject both API_URL and SECONDARY_API_URL alongside ActivatedRoute. However, this approach would mean duplicating a substantial amount of boilerplate logic across each 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 code below looks far cleaner, and the approach it demonstrates is exactly what we're about to build shortly:
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);
}
}
To make this happen, we introduce an InjectionToken named CONDITIONAL_API_URL, which is responsible for picking the correct api url. This token leverages Angular’s inject function to bring in the three required dependencies, then selects the appropriate url depending on whether the secondary QueryParam is present.
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’s no requirement to supply CONDITIONAL_API_URL; it can simply be injected into FooService inline, as demonstrated. Whenever the app boots with the secondary query parameter present in the URL, SECONDARY_API_URL is selected. In all other cases, API_URL is used.
Conclusion
Injection tokens are highly versatile, and when paired with Angular’s inject() function—which follows the service locator pattern—they allow for combining and embedding logic directly into tokens. To dive deeper into Angular’s inject() function, refer to this article.
A stackblitz example with the full source code is available here. Keep in mind that external javascript files cannot be used in those stackblitz projects at the moment, so the contents of the env.js file have been embedded directly into the index.html.
Thanks for the reviewers
Special thanks to Wim Holvoet for the injection token idea.

•