Baking Configuration into your Angular App Pie
Paul Mooney
Posted on October 25, 2019
In this post I'm going to talk about some of the best ways to get your configurations to your Angular app. Just note, this isn't a post about Angular framework level configurations, this is about how the features you're developing receive your configurable values.
Where's my Backend API Server?
Most SPAs need a backend API server, so when development starts there's the question of "how do I tell my app where my API server is?" The answer is that you don't. Your app should assume the API server is served from the same host as the app itself. It will only use relative URLs (in this case "relative" means no protocol, host, or port specified) to call the API server.
For example:
@Injectable({
providedIn: 'root'
})
export class ServerTimeService {
constructor(private httpClient: HttpClient) { }
getTime(): Observable<string> {
// Calls relative path `/api`. No host in the URL here.
return this.httpClient.get('/api/servertime')
.pipe(map((data: any) => data.servertime));
}
}
This is nice and clean, and avoids CORS complications and issues.
How do we achieve this? With Reverse Proxies.
Let's look at the scenario where your backend API server sits at http://myinternalhost:8080/api
and we want the app to be able to make requests only to paths starting with /api
. Here's how you can configure reverse proxies for development and when deployed:
Proxy Server during Development
When a project is generated using Angular CLI it uses webpack (at least at the time of writing this) which includes a dev server that hosts the app and watches for changes when we run ng serve
(or npm start
if you're using the Angular CLI defaults). This server also includes a reverse proxy which can be configured via proxy.conf.js
or proxy.conf.json
file. You can read more about it in the Angular CLI repo. I prefer the 'js' version of the file since it gives us more flexibility.
Given our example scenario for getting requests from the relative path /api
to the absolute path http://myinternalhost:8080/api
, we can setup our proxy.conf.js
in the root of our project folder like so:
const PROXY_CONFIG = {
'/api': {
'target': 'http://myinternalhost:8080',
'secure': false,
'logLevel': 'debug',
'changeOrigin': true
}
};
module.exports = PROXY_CONFIG;
And alter the "start" npm script to tell it to use the proxy.conf.js
file:
"start":"ng serve --proxy-config proxy.conf.js"
Of course it would be better if the target
value was not hardcoded to a specific server in a file that we're going to be checking into version control, so we can use an environment variable instead. Let's make the above snippet better:
const PROXY_CONFIG = {
'/api': {
'target': process.env.API_SERVER,
'secure': false,
'logLevel': 'debug',
'changeOrigin': true
}
};
module.exports = PROXY_CONFIG;
The environment variable can be passed via commandline API_SERVER=http://myinternalhost:8080 npm start
.
Reverse Proxy when Deployed
When you're deploying your application, you won't have webpack's dev-server to use as a reverse proxy so you'll need a separate standalone one. Popular options for reverse proxies are webservers like NGINX or Apache HTTP Server. These serve other purposes as well such as handling HTTPS, load balancing, or if you're not using Server Side Rendering (https://angular.io/guide/universal) they can be used to serve your Angular app's static assets. So it's likely you'll need one of these anyways.
The key idea here is that the reverse proxy is the single point for traffic to and from the browser for both requests to your app, and requests to the API server.
Here's a snippet of nginx configuration that forwards traffic to your app, and to our http://myinternalhost:8080
API server:
server {
listen 80;
server_name localhost;
# Reverse proxy all traffic to the Angular app
location / {
proxy_pass http://localhost:4000;
}
# Reverse proxy all traffic starting with `/api` to the backend API server
location /api {
proxy_pass http://myinternalhost:8080;
}
}
NGINX itself can be configured to use environment variables as mentioned on its Docker Hub page.
What about Server Side Rendering?
In server side rendering (SSR), your Angular app's code is running on the server similar to how it would run in the browser, complete with the API calls it needs to make but with a few exceptions. One of those exceptions is that relative URLs are meaningless on the server. Servers want absolute URLs. So it turns out that our app does need that absolute URL to the backend API afterall.
Luckily, when rendering on the server, we're not in a context where we need to worry about CORS, and we are in a context where your code can read environment variables. So our example HttpClient request can be altered to look like this:
@Injectable({
providedIn: 'root'
})
export class ServerTimeService {
constructor(private httpClient: HttpClient, @Inject(PLATFORM_ID) private platformId) { }
getTime(): Observable<string> {
const path = '/api/servertime';
// Make URL absolute only if on the server
const url = isPlatformServer(this.platformId) ? process.env.API_SERVER + path : path;
return this.httpClient.get(url)
.pipe(map((data: any) => data.servertime));
}
}
This doesn't mean we can ditch the reverse proxy setup, we still need that when the app is running in the browser. This is just an extra consideration to make when leveraging SSR.
Note:
For this to compile, you will also need to install node types via npm i -D @types/node
and then add "node"
to the compilerOptions.types
array of the the tsconfig.app.json
file.
Environment Variables vs Environment.ts
Let's imagine another scenario where your Angular app has a typeahead search in it, and it needs a debounce time to decide when the user has stopped typing and it's safe to make an API call. Kind of like this article describes. We want to make the debounce time configurable.
It'd be tempting to use the Environment.ts
and Environment.prod.ts
as the configuration point for this debounce time, but you probably shouldn't. Actually, just don't. It's a violation of the third factor of The Twelve-Factor App. The short of it is that if you are using a version controlled file in your app to store configuration then your app has to be rebuilt and redeployed just to affect a configuration change. Sounds like hardcoding not configuration. This is fine for the world of Infrastructure as Code and GitOps but it is not ideal for applications.
In general you probably won't use the Environment.ts
files much unless there are different modes your application needs to be built in. If you find yourself writing Environment.staging.ts
or Environment.qa.ts
files, you're doing it wrong.
So how do you configure this 'debounce' time in the app? With Environment Variables! How do we use environment variables in an app that mostly runs in the browser? Serve them via API server.
There are multiple ways to do this. We'll take the approach that we're using a purpose built "Config" REST endpoint just for this Angular app.
Sending environment variables during Development
A quick and easy way to create a Config REST endpoint to use during development is to leverage the webpack's proxy server. We can create a faux backend inside the proxy.conf.js
file like so:
const PROXY_CONFIG = {
'/config': {
'bypass': function (req, res, proxyOptions) {
switch (req.url) {
case '/config':
// Send an map of config values
res.end(JSON.stringify({
DEBOUNCE_TIME: process.env.DEBOUNCE_TIME || 500 // Read from environment or default to 500
... // Other config values here
}));
return true;
}
}
}
... // Other proxy settings
};
export PROXY_CONFIG;
From there it's just a matter of making a call to this /config
endpoint just like any other endpoint.
this.httpClient.get('/config');
You can start your development server with an environment variable like so DEBOUNCE_TIME=300 npm start
Sending environment variables when Deployed
For this, you'd probably just have to build a separate server, perhaps using something like Express. However, if you're leveraging server side rendering then you'll probably already have a server in the form of the server.ts
file (likely generated by a schematic like @nguniversal/express-engine). This is a good place to add a little extra functionality to serve up configuration read from server side environment variables in a similar manner to how it's done in the proxy.conf.js
example.
Add the following to the server.ts
file used for SSR:
app.get('/config', (req, res) => {
res.status(200).send({
DEBOUNCE_TIME: process.env.DEBOUNCE_TIME || 500 // Read from environment or default to 500
... // Other config values here
});
});
During server side rendering, when the code is executing on the server you won't necessarily need to call this API (though you could) since you can just directly access the environment variables from within code. To keep things simple, it's probably best to hide how all of your configuration values are retreived behind a single "Config" Angular service:
@Injectable({
providedIn: 'root'
})
export class ConfigService {
constructor(private httpClient: HttpClient, @Inject(PLATFORM_ID) private platformId) {}
getConfig(): Observable<any> {
// Direct, speedy access to environment variables when on server.
if (isPlatformServer(this.platformId)) {
return of({
DEBOUNCE_TIME: process.env.DEBOUNCE_TIME
});
}
// Otherwise from the brwoser call the `/config` API.
return this.httpClient.get('/config');
}
}
Avoid Depending on Transferstate to Transport your Configuration
When using server side rendering, It may be tempting to avoid setting up a "Config" REST service like the one above and just leverage transfer state to gather values from environment variables on the server and send them to the client. This may or may not work for you but if you're enabling Progressive Web App then a good deal of the time server side rendering won't even come into play since the app is rendered from javascript and other assets cached in the browser, bypassing SSR completely. Since there's no SSR happening in a PWA, there's no transferstate, so it's not a good idea to make it the sole medium for transporting configuration values.
The right time to call your Configuration API endpoint
There are different situations where you may need to call a configuration API in the lifecycle of your app. The earlier it's called the better, but it can also get more complex. These are some of the places where you could call the config API from:
On Demand, maybe leveraging a behaviour subject
This is like the title says, call it only when you need. This is ideal when you require configuration values for some of the views or components you're developing. You can call the config API from one of the lifecycle hooks of your components.
Perhaps use something like a Replay Subject to prevent multiple or competing calls going to the config API at once and to cache your config values.
From the Angular APP_INITIALIZER hook
An APP_INITIALIZER function gets called during Angular's startup. This is likely the place you want to execute your config retrieval if some of those configurations are central to the app. Like say, if they relate to how you might configure a global aspect of the app such as internationalization, or possibly affect some change in routing, or maybe if you prefer the app to just fail fast when there is an invalid configuration instead of finding out later when the config value is finally used.
You can read more about the APP_INITIALIZER.
Again, it's probably good to wrap the config API call in a Replay Subject just so that its results can be cached for later.
Before Angular Starts
This is the earliest time to retrieve configuration: before anything Angular begins to bootstrap. This is good for situations where you need these values even earlier than APP_INITIALIZER allows. Examples might be if you need them to configure a custom HttpInterceptor or if you have a special Error Handler that needs an API key to a logging service.
The place to make this call is in the main.ts
file. On return, store the results in local storage so that they can be retrieved when needed. Note that angular service such as HttpClient won't be available so the browser basics like fetch
or XMLHttpRequest
will have to do.
Example main.ts
file:
if (environment.production) {
enableProdMode();
}
document.addEventListener('DOMContentLoaded', async () => {
const response = await fetch('/config');
if (response.status === 200) {
const result = await response.text();
localStorage.setItem('config', result);
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
}
});
.env Files
One last bonus tidbit of information: It can be tedious to setup environment variables in the command line when developing. Especially if there's a lot of them. The answer to this problem is the .env
file.
It's a simple file where each line is an environment variable assignment in the format VARIABLE_NAME=value
. And it supports comments!
The .env
file works out of the box in some runtimes, like for docker-compose, but doesn't work out of the box in node.js. You'll need to install the library dotenv as a dev dependency: npm i -D dotenv
and then have it loaded up.
To load it in your proxy.conf.js
, just add the following line to the top of the file.
require('dotenv').config();
To load it for SSR, alter the npm script called "serve:ssr" to the following:
"serve:ssr":"node -r dotenv/config dist/server"
Finally be sure .env
file entry is added to your .gitignore
file. This file is for your local development, it would be really annoying if your settings were regularly and unexpectedly clobbered by someone else's changes whenever you're pulling the latest.
Wrapping up
To summarize what we've learned here about getting configuration to your Angular app:
Use a reverse-proxy to "host" your Angular app and Backend APIs from the same server, don't try to configure where that backend API is in your Angular app.
You may have very frontend specific configurations that aren't appropriate to serve from your existing business oriented backend APIs. If so, create a simple config API by hijacking your webpack dev-server during development, and by hijacking your server.ts file if your're using SSR.
Environment Variables are a good medium to set config values from the server side.
You probably won't need
Environment.ts
files as much as you think.There are various times to call your config API. Pick one.
Don't forget the
.env
files
Hope this was a good read. Not all of it will be appropriate for your project, but I'm sure some of it will be.
Sample project source, and this blog in the works, can be found here
Posted on October 25, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 24, 2024