Runtime configuration for Single-Page Applications

When we are working on developing a large and distributed system, the frontend is separated and normally presented as a single-page application (SPA) or a set of micro frontends (set of SPAs). Normally, an SPA is immutable and served from some static storage, and not served from the same app that provides the backend APIs or using server-side rendering (SSR) to generate them at runtime. This means you don't have access to environment variables to configure your application during deployment or at runtime.

This is a quite common problem with quite common solutions.

Build-time configuration: Create different app configurations and build different artifacts for each of the environments. But in that case, you will get a separate set of artifacts and can’t guarantee that your production application is exactly the same as what you use for testing.

Runtime configuration: Instead of building separate artifacts, you can include some placeholders for all configurations and replace them with actual values during application deployment, like here. This approach is much better from my point of view, but also has some cons. For example: increased complexity in deployment scripts, potential security risks if placeholders are not managed properly, and harder to debug issues related to configuration values.

Runtime configuration with external configuration: In this approach, we can move our application configuration to a separate external file (let’s call it config.json) and fetch this file during application startup. This option is preferable for many reasons: it is easier to manage and update configuration without rebuilding the application, and it simplifies the deployment process by separating configuration from the application code.

Here is a simple example implementing this approach for a React application.

First of all, you need to create the config and put it into the public folder:

public/config.json
1
2
3
4
5
{
"settingKey1": "value1",
"settingKey2": "value2",
"settingKey3": "value3"
}

Then, you need to create a few utils to help you work with the configuration:

utils/config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let config = null;

export const loadConfig = async () => {
if (config) return config; // Return cached config if already loaded.

const response = await fetch('/config.json');
if (!response.ok) {
throw new Error(`Failed to load configuration: ${response.statusText}`);
}

config = await response.json();
return config;
};

export const getConfig = () => {
if (!config) {
throw new Error('Configuration not loaded yet. Call loadConfig() first.');
}
return config;
};

And, finally, attach the config to your application:

App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React, { useEffect, useState } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import { loadConfig, getConfig } from './utils/config'; // import config here
import Home from './pages/Home';

function App() {
const [loading, setLoading] = useState(true);

useEffect(() => {
const initializeConfig = async () => {
try {
await loadConfig();
setLoading(false);
} catch (error) {
console.error('Error loading configuration:', error);
}
};

initializeConfig();
}, []);

if (loading) {
return <div>Loading configuration...</div>;
}

return (
<Router>
<div>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</div>
</Router>
);
}

export default App;

Using the configuration inside your application is pretty simple:

components/Example.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import { getConfig } from '../utils/config';

const Example = ({ style }) => {
const handleExample = () => {
const appConfig = getConfig();
console.log(appConfig.settingKey1);
console.log(appConfig.settingKey2);
console.log(appConfig.settingKey3);
};

return (
<div style={style}>
<button onClick={handleExample}>Log Config</button>
</div>
);
};

export default Example;

Bonus content: How to deal with configuration in case of Kubernetes

In case you serve your static applications from a simple container with an HTTP server (nginx for example), you can easily mount the configuration file from a ConfigMap.

ConfigMap:

templates/configmap-files.yaml
1
2
3
4
5
6
7
8
{{- template "common.configmap" (list . "application_files.configmap") -}}
{{- define "application_files.configmap" -}}
metadata:
name: {{ template "common.fullname" . }}-files
data:
config.json: |+
{{ .Values.configuration | toJson }}
{{- end -}}

Deployment:

templates/deployment.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{{- template "common.deployment" (list . "application.deployment") -}}
{{- define "application.deployment" -}}
spec:
template:
metadata:
annotations:
checksum/files: {{ include (print $.Template.BasePath "/configmap-files.yaml") . | sha256sum | trunc 63}}
spec:
volumes:
- name: files
configMap:
name: {{ template "common.fullname" . }}-files
containers:
-
{{ include "common.container.tpl" . | indent 8 }}
volumeMounts:
- name: files
mountPath: /app/config.json
subPath: config.json

{{- end -}}

Do not forget to connect the library chart to render this template correctly:

Chart.yaml
1
2
3
4
5
6
7
8
9
10
apiVersion: v2
name: example-spa
description: A Helm chart for Example SPA
type: application
version: 1.0.0
appVersion: "1.0.0"
dependencies:
- name: common
repository: https://kharkevich.github.io/helm-generic/
version: 2.1.0