, 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:
SOCIAL SHARE CARD GENERATOR