Angular Without the Ceremony
I have used Angular since its early versions. Over time I have seen the framework accumulate ceremony — NgModules, complex provider chains, state management libraries that require their own learning curve just to store a simple flag. For most applications, it is too much.
The RAD-System frontend is my attempt to use modern Angular the way it was meant to be used: lean, modular, and without dependencies on libraries that solve problems I do not have. The source is on GitHub.
1. Runtime Configuration: Build Once, Deploy Anywhere
This is one of the patterns I am most proud of in this system. Angular's built-in environment files are compiled into the build — if you need to change the API URL after building, you rebuild. In a multi-environment setup (development, staging, production) this is not acceptable.
My solution: load app-config.json before the application bootstraps. The same compiled bundle deploys to any environment. You change environments by swapping a JSON file.
// Load configuration before bootstrapping
const loadConfig = async () => {
try {
const dt = new Date().getMilliseconds();
// Cache-busting to ensure fresh config
const response = await fetch(`./assets/config/app-config.json?ver=${dt}`);
const config = await response.json();
// Store config synchronously for global access
StoreService.setConfig(config);
bootstrapApplication(AppComponent, {
...appConfig,
providers: [provideZoneChangeDetection(), ...appConfig.providers],
});
} catch (error) {
console.error("Application cannot start without configuration:", error);
}
};
loadConfig();
2. State Management Without NgRx: StoreService
I tried NgRx on several projects. For most applications its complexity is disproportionate to the problem it solves. I built StoreService as a lightweight alternative — backed by BehaviorSubject, using browser storage for persistence, and accessible as a static class anywhere in the application including outside Angular's DI system.
@Injectable({ providedIn: "root" })
export abstract class StoreService {
private static PREFIX: string = "RAD.";
// Static access allows usage outside Angular's DI (e.g., in functions)
static set(key: string, value: any, persistent: boolean = false) {
const storage = persistent ? localStorage : sessionStorage;
storage.setItem(this.PREFIX + key, value);
}
static getApiUrl(): string {
return this.CONFIG.apiUrl;
}
}
For applications that genuinely need reactive state across many components, NgRx or Signals are the right answer. For the other 80% of cases, this is enough and costs nothing to understand.
3. Centralised API Management
Every HTTP call in the application goes through a single ApiService. It automatically prepends the runtime API URL from StoreService, sets the standard headers (Authorization, Content-Type), and centralises error handling. No HttpClient calls scattered across components — one entry point, one place to change behaviour globally.
4. Components Built to Be Reused
The system ships with a set of functional ready-to-use components in src/app/Features:
- Dynamic Menu — builds the navigation tree at runtime, filtering items based on the user's role (ACL) and the current configuration. Add a new module, add a menu entry in config — no code change.
- Token Status — a session watchdog that shows JWT validity and warns the user before their token expires. I got tired of users being logged out mid-work without warning.
- Lang Picker — instant interface language switching using
Transloco. Add a translation file, the picker updates automatically.