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.