Using a server’s environment variables in Angular

Yannick Haas
medialesson
Published in
6 min readApr 2, 2024

--

This is part of a multi-post series detailing how to consume environment variables in an Angular application, replacing the traditional environment.ts files. The other posts can be found here:

In a cloud environment, you might find that your application has to be very flexible. Different URLs for each environment, different Client-IDs and Client-Secrets, and so on. Ideally we don’t want to hard-code this information in our application and instead fetch the data before we bootstrap Angular, to be able to have the value inside an injection token. This is actually possible, but the main.ts has to be heavily modified for it to work.

In my last post we created an endpoint called config in a Node application, which also serves an Angular client. We’re going to use this as an example.

Fetching data and using it in an injection Token before Angular is bootstrapped

For this we can use fetch in main.ts . This also allows us to create an injection token with the configuration, which can then be easily injected in services or components that need to read the app’s configuration (like an OIDC client for example).

First of all we fetch the configuration from our /config endpoint before bootstrapping:

fetch('/config')
.then((response: Response) => {
if (response.ok) {
// Get body
return response.json();
} else {
// Fail startup
return Promise.reject(response);
}
})

If we use fetch without any URL and only a path, it’ll use the current URL origin. That’s also the reason we’ll never have to change the URL for an environment.

In the next then statement, we either have our AppConfiguration, which we got from the body, or a Response object with more detailed information about the request’s failure. To distinguish between these two types (as only one can be used for our application), we simply check, if a property is present in the object. In this case we’ll check for angularEnvironment as we need it for enableProdMode() anyway.

fetch('/config')
.then(
// ...
)
.then((response: AppConfiguration | Response) => {
// Check if configuration is valid
if ('angularEnvironment' in response) {
const appConfig = response as AppConfiguration;

// Always assume production mode, if it's not explicitly set to development
if (appConfig.angularEnvironment !== 'development') {
enableProdMode();
}
}
});

Before we can bootstrap the application, we need to create our injection token constant. In the same file we’ve got the interface for AppConfiguration we’re going to create the Injection Token by adding the following code:

export const APP_CONFIGURATION_TOKEN = new InjectionToken<AppConfiguration>('AppConfiguration');

So anytime we need it in a service or component, we can simply get it in the constructor or as a class member with:

// using inject()
private readonly appConfig: AppConfiguration = inject(APP_CONFIGURATION_TOKEN);

// In constructor
constructor(@Inject(APP_CONFIGURATION_TOKEN) appConfig: AppConfiguration) {}

Finally, we’ll bootstrap the application after setting the mode and provide the constant appConfig as an injection token in our application. To avoid confusion between the appConfig constant we declared here and the one generated by Angular, we’re going to delete the app/app.config.ts file and provide the options directly in main.ts along with our injection token:

// ...
.then(
// ...
bootstrapApplication(AppComponent, {
providers: [
{
provide: APP_CONFIGURATION_TOKEN,
useValue: appConfig,
},
provideRouter(appRoutes),
],
}).catch((err) => console.error(err));
);

That’s what the main.ts of our client should look like:

import {
APP_CONFIGURATION_TOKEN,
AppConfiguration,
} from '@angular-environment-variables/app-configuration';
import { enableProdMode, importProvidersFrom } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideRouter } from '@angular/router';
import { appRoutes } from './app/app.routes';
import { HttpClientModule } from '@angular/common/http';

// Fetch configuration from runtime-host
fetch('/config')
.then((response: Response) => {
if (response.ok) {
// Get body
return response.json();
} else {
// Fail startup
return Promise.reject(response);
}
})
.then((response: AppConfiguration | Response) => {
// Check if configuration is valid
if ('angularEnvironment' in response) {
const appConfig = response as AppConfiguration;
// Always assume production mode, if it's not explicitly set to development
if (appConfig.angularEnvironment !== 'development') {
enableProdMode();
}
bootstrapApplication(AppComponent, {
providers: [
{
provide: APP_CONFIGURATION_TOKEN,
useValue: appConfig,
},
importProvidersFrom(HttpClientModule),
provideRouter(appRoutes),
],
}).catch((err) => console.error(err));
}
});

That’s all we have to do to fetch data before Angular gets bootstrapped and using it as a value for an injection token. You can now easily inject it into your components and use it the same way you would use an environment.ts file as its synchronous.

Let’s build an example application

Before we add our own code, we’re going to delete some sample files and code, which were created by Nx. First of all delete the file apps/angular-environment-variables/src/app/nx-welcome.component.ts . Next we have to remove the import in app.component.ts and HTML-Tag in app.component.html .

Your app.component.* files should look like this:

<!-- app.component.html -->
<router-outlet></router-outlet>
// app.component.ts
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';

@Component({
standalone: true,
imports: [RouterModule],
selector: 'angular-environment-variables-root',
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {}

Make sure to set the html and body to be full height and width in styles.scss:

/* styles.scss */
body,
html {
height: 100%;
width: 100%;
margin: 0;
}

Now generate an example component with

nx g @nx/angular:component --name=Example --directory apps/angular-environment-variables/src/app/example

Add the routes:

// app.routes.ts
import { Route } from '@angular/router';

export const appRoutes: Route[] = [
{
path: '',
loadComponent: () => import('./example/Example.component').then((m) => m.ExampleComponent),
},
{
path: '**',
redirectTo: '',
},
];

If you serve your application now and navigate to it, you should see Example works! . This indicates that everything works correctly and we already got a response from the Runtime Host as otherwise our Angular application wouldn’t even load.

Next up, we’re injecting the APP_CONFIGURATION_TOKEN in our ExampleComponent , which we will then use to set a background color of a div .

// Example.component.ts
// ...
export class ExampleComponent {
public readonly appConfig: AppConfiguration = inject(APP_CONFIGURATION_TOKEN);
}
<!-- Example.component.html -->
<div
style="height: 100%; width: 100%"
[ngStyle]="{ 'background-color': appConfig.backgroundColor }"></div>

If you serve the application now, you should see a div taking up all the available space with a light blue background. Play around with the .env file and set different background colors. Each time you reload the page (and restart the application) your set color should appear (if it’s a valid one).

Finally, we’re going to fetch some data from the example API we configured in our .env . For this we need to create a service. I’ll call it APIService.

nx g @nx/angular:service --name=API --project=angular-environment-variables

Inside our APIService we’re going to inject our token using @Inject() and assign the private class member apiUrl the value of appConfig.apiUrl with a slash added in case the URL ends without one.

// api.service.ts
export class APIService {
private readonly apiUrl: string;
constructor(
@Inject(APP_CONFIGURATION_TOKEN) appConfig: AppConfiguration,
private readonly http: HttpClient
) {
if (appConfig.apiUrl !== null) {
// Ensure that the API URL ends with a trailing slash
this.apiUrl = appConfig.apiUrl.endsWith('/')
? appConfig.apiUrl
: `${appConfig.apiUrl}/`;
} else {
this.apiUrl = '';
console.error('Provided API URL was null. Please check configuration.')
}
}
}

Next we’re going to create the interface for the JSON we’re receiving from our API.

// random-dog.model.ts
export interface RandomDogApiResponse {
fileSizeBytes: number;
url: string;
}

Now add the method to return our Observable inside APIService:

getRandomDog(): Observable<RandomDogApiResponse | undefined> {
// Filter out mp4 and webm as they're not compatible with img tag
return this.apiUrl
? this.http.get<RandomDogApiResponse>(
`${this.apiUrl}woof.json?filter=mp4,webm`
)
: of(undefined);
}

Finally inject the APIService inside ExampleComponent and call the API. We’re going to do this using Signals as this allows us to use ChangeDetection.OnPush easily. To do this we need to modify ExampleComponent to look like this:

// Example.component.ts
@Component({
selector: 'angular-environment-variables-example',
standalone: true,
imports: [CommonModule],
templateUrl: './Example.component.html',
styleUrl: './Example.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExampleComponent {
public readonly appConfig: AppConfiguration = inject(APP_CONFIGURATION_TOKEN);
public readonly randomDog: Signal<RandomDogApiResponse | undefined> =
toSignal(this.apiService.getRandomDog());

constructor(private readonly apiService: APIService) {}
}
<!-- Example.component.html -->
<div
style="height: 100%; width: 100%; display: flex"
[ngStyle]="{ 'background-color': appConfig.backgroundColor }">
<img *ngIf="randomDog() as dog" style="height: 75%; width: 75%; object-fit: contain; margin: auto" [src]="dog.url" />
</div>

Assuming we’ve created the corresponding .env file (read more about how to do this here), if we serve the application now, we should get our selected background color with a cute dog in the center of the page.

--

--