Why You Should "Pull Your App Up by Its Own Bootstraps"
The English idiom "to pull oneself up by one's bootstraps" (in Italian: tirarsi su per le stringhe degli stivali) describes an impossible task: lifting yourself off the ground by pulling your own laces. In software engineering, however, bootstrapping is very real. It’s that critical moment where an application loads its configuration, sets up its environment, and prepares itself for the user—all before the first component even renders.
The Standard Approach: APP_INITIALIZER
Standard literature often suggests using the APP_INITIALIZER token. This allows you to hook into the Angular bootstrap process and run a function (usually returning a Promise or Observable) before the app finishes initializing. It looks something like this:
// The "Standard" Way
{
provide: APP_INITIALIZER,
useFactory: (configService: ConfigService) => () => configService.loadConfig(),
deps: [ConfigService],
multi: true
}
While functional, this happens inside the Angular lifecycle. This means your services and components are already being instantiated by the DI (Dependency Injection) container. If your configuration is slow or fails, you can run into "flickering" UI states or race conditions where a service tries to access a config value that hasn't arrived yet.
A Smarter Approach: The "Hot Config" Method
In my work with the RAD Framework, I’ve refined a method that makes the application "smart" from millisecond zero by moving the logic outside the Angular lifecycle.
1. Hot Config (JSON-based)
Instead of hardcoding values in environment files (which require a rebuild for every change), we use a standalone app-config.json. This allows for "Hot Configuration"—changing API endpoints or feature flags on the fly.
{
"appName": "RAG System",
"apiUrl": "http://localhost:3000/api/v1",
"logMode": "console",
"siteMode": "development",
"version": "1.0.0",
"langs": [
{ "code": "en", "label": "ENG", "description": "English", "langEn": "English" },
{ "code": "it", "label": "ITA", "description": "Italiano", "langEn": "Italian" }
],
"google": {
"clientId": "YOUR_GOOGLE_CLIENT_ID",
"redirectUri": "http://localhost:4200",
"button": { "theme": "outline", "size": "large", "text": "continue_with", "logo_alignment": "left" },
"analyticsId": "YOUR_GOOGLE_ANALYTICS_ID"
},
"uploadConfig": { "maxFileSize": 50, "maxFileCount": 5, "acceptedFileTypes": "*/*", "allowMultiple": false },
"menus": {
"guest": [
{ "label": "home", "icon": "home", "route": "/home" },
{ "label": "Login", "icon": "login", "route": "/login" }
],
"user": [
{ "label": "Home", "icon": "home", "route": "/welcome" }
],
"admin": [
{ "label": "_DIVIDER_" },
{ "label": "Config", "icon": "settings", "route": "/admin-config" }
]
}
}
2. The Static StoreService
To avoid the "chicken and egg" problem of Dependency Injection, I use a Static StoreService. By using static properties, the configuration becomes a global "Source of Truth" that is available even before Angular's DI container is fully ready.
import { Injectable } from "@angular/core";
import { BehaviorSubject } from "rxjs";
import { I18nLang, IAppConfig, IMenuItem, SiteModeType } from "../../Models/config.model";
/**
* The app configuration is loaded when angular starts in main.ts
* This service provides access to the configuration
* and manages application state in localStorage/sessionStorage
*/
@Injectable({
providedIn: "root",
})
export abstract class StoreService {
private static PREFIX: string = "MYAPP
private static CONFIG = {} as IAppConfig;
// Token change observable
private static tokenSubject = new BehaviorSubject<string>("");
public static tokenChange$ = StoreService.tokenSubject.asObservable();
constructor() {}
static set(key: string, value: any, persistent: boolean = false) {
key = this.PREFIX + key;
if (persistent) localStorage.setItem(key, value);
else sessionStorage.setItem(key, value);
}
static get(key: string): any {
key = this.PREFIX + key;
return localStorage.getItem(key)
? localStorage.getItem(key)
: sessionStorage.getItem(key);
}
static remove(key: string) {
key = this.PREFIX + key;
localStorage.removeItem(key);
sessionStorage.removeItem(key);
}
static clear() {
localStorage.clear();
sessionStorage.clear();
}
static getAll() {
const allItems = { ...localStorage, ...sessionStorage };
const filteredItems: { [key: string]: string } = {};
for (const key in allItems) {
if (key.startsWith(this.PREFIX)) {
filteredItems[key.replace(this.PREFIX, "")] = allItems[key];
}
}
return filteredItems;
}
// TOKENS SECTION
static setJwtToken(token: string) {
if (!token) {
this.remove("auth_token");
this.tokenSubject.next(""); // Emit empty token (logout)
return;
}
this.set("auth_token", token);
this.tokenSubject.next(token); // Emit new token (login/refresh)
}
static getJwtToken(): string {
return this.get("auth_token") || "";
}
// CONFIG SECTION
static setConfig(config: IAppConfig) {
this.CONFIG = config;
}
private static getConfig(): IAppConfig {
return this.CONFIG;
}
static getApiUrl(): string {
return this.getConfig().apiUrl;
}
// add static functions or properties for every options you need
}
3. The "Pre-Flight" Boot in main.ts
This is the "smart" part. Instead of letting Angular start and then asking for the config, we fetch the config in main.ts and then trigger the bootstrap process.
import { provideZoneChangeDetection } from "@angular/core";
/// <reference types="@angular/localize" />
import { bootstrapApplication } from "@angular/platform-browser";
import { appConfig } from "./app/app.config";
import { AppComponent } from "./app/app.component";
import { StoreService } from "./app/Core/services/store.service";
// Load configuration before bootstrapping the application
const loadConfig = async () => {
try {
const dt = new Date().getMilliseconds();
const response = await fetch(`./assets/config/app-config.json?ver=${dt}`);
if (!response.ok) {
throw new Error(
`Failed to load configuration: ${response.status} ${response.statusText}`,
);
}
const config = await response.json();
StoreService.setConfig(config);
// Bootstrap application after config is loaded (or failed to load)
bootstrapApplication(AppComponent, {
...appConfig,
providers: [provideZoneChangeDetection(), ...appConfig.providers],
}).catch((err) => console.error("Application bootstrap failed:", err));
} catch (error) {
console.error(
"Fatal error: Application cannot start without configuration:",
error,
);
}
};
// Execute the configuration loading
loadConfig();
Why is this better?
Zero Flickering: No "loading" spinners while waiting for settings.
No Undefined Errors: You don't need to check if(config) in every constructor.
Environment Agnostic: The same build works in Dev, Staging, and Prod just by swapping the JSON file.
Conclusion:
Bootstrapping shouldn't be an afterthought. By moving your configuration logic to the very edge of your application's entry point, you create a more robust, flexible, and predictable system.
What you think about this approach? Write your comments.
This is my first post so be kind. :)
References:
My RAD Framework
Source Code on Github

Top comments (0)