Shahathir (•◡•)

24 · Batu Caves, Selangor, Malaysia · 🇲🇾

professionally distracted

My Career Journey

  1. Financial Risk Group logo

    Financial Risk Group

    1yrs 3mos

    Assistant Software Developer

    Jun 2025 – Present

  2. Estee Lauder Companies logo

    Estee Lauder Companies

    6mos

    Software Engineer Intern

    Sep 2024 – Mar 2025

Tools & Platforms

TypeScript
JavaScript
Java
Python
PHP
Go
HTML5
CSS3
React
Next.js
Vite
Angular
Redux
React Router
Tailwind CSS
shadcn/ui
Material UI
Sass
Bootstrap
React Native
Node.js
TypeScript
JavaScript
Java
Python
PHP
Go
HTML5
CSS3
React
Next.js
Vite
Angular
Redux
React Router
Tailwind CSS
shadcn/ui
Material UI
Sass
Bootstrap
React Native
Node.js
Bun
Django
FastAPI
Spring Boot
Java Servlets
Express
PostgreSQL
MySQL
MS SQL Server
SQLite
Appwrite
Docker
AWS
Cloudflare
Nginx
Vercel
Netlify
DigitalOcean
Git
DBeaver
Postman
Bun
Django
FastAPI
Spring Boot
Java Servlets
Express
PostgreSQL
MySQL
MS SQL Server
SQLite
Appwrite
Docker
AWS
Cloudflare
Nginx
Vercel
Netlify
DigitalOcean
Git
DBeaver
Postman

Words I Live By

Shahathir is currently not listening to anything
Shahathir is currently not listening to anything

2026 © shahathir.me

Changelogs · Old site

  • AboutAbout
  • ThoughtsThoughts
  • TILTIL
  • BookmarksBookmarks
  • ExperienceExperience
  • ProjectsProjects
  • AccoladesAccolades
  • PhotographyPhotography
  • SongsSongs
  • StatsStats
  • UsesUses
  • ChatChat
  • Resume BuilderResume Builder
  • AboutAbout
  • ThoughtsThoughts
  • TILTIL
  • BookmarksBookmarks
  • ExperienceExperience
  • ProjectsProjects
  • AccoladesAccolades
  • PhotographyPhotography
  • SongsSongs
  • StatsStats
  • UsesUses
  • ChatChat
  • Resume BuilderResume Builder
April 18, 2026

One build, every environment — runtime config the native Angular way

The value wasn't the config library I removed. It was the lifecycle hook that blocks bootstrap until config loads, which makes config synchronous everywhere and kills a whole class of race.

We ship a single build of an Angular app to several environments. The API base URL, feature flags, and a few other knobs differ per environment. You can bake those in at build time — but then you need one build per environment, and "the thing we tested" isn't "the thing we deployed."

The alternative is to fetch a config.json at startup. The same compiled artifact runs anywhere; you change the file, not the build. We'd been doing this through a third-party library, and I replaced it with about twenty lines of native Angular. The interesting part was realizing the library's actual value was one lifecycle hook.

That hook is provideAppInitializer: it runs an async function before the app bootstraps, and bootstrap waits for the returned promise. So you fetch the config, validate it, stash it — and because bootstrap blocked, everything that renders afterward can read config synchronously.

bootstrapApplication(AppComponent, {
  providers: [provideAppInitializer(loadRuntimeConfig)],
});
 
export async function loadRuntimeConfig(): Promise<void> {
  const svc = inject(RuntimeConfigService);
  const res = await fetch("/assets/config.json");
  if (!res.ok) throw new Error(`Config load failed: ${res.status}`);
  svc.set(assertRuntimeConfig(await res.json())); // validates shape; throws if wrong
}
 
@Injectable({ providedIn: "root" })
export class RuntimeConfigService {
  private config = signal<AppConfig | null>(null);
  readonly apiBase = computed(() => this.config()?.apiBase ?? ""); // "" => relative URLs
  set(c: AppConfig) { this.config.set(c); }
}

That "synchronous after load" property is the whole point, and it's what the old approach got wrong. The library exposed config as an observable stream. Anything that read config before the stream emitted got nothing — a race that showed up as intermittent, environment-specific "why is this undefined" bugs. Blocking bootstrap deletes that entire category: by the time a single component renders, the config is already there. No configReady$, no guards waiting on config, no defensive ?? defaultValue scattered around.

Two smaller things I'd repeat:

  • Fail loud, at boot. A hand-written type guard (assertRuntimeConfig) checks every required field and throws at startup if the file is malformed. A screaming failure the instant the app loads beats a mysterious undefined three screens deep.
  • Empty-string fallback for the API base. apiBase() falls back to "" on purpose, so requests stay relative and work behind a reverse proxy that terminates on the same origin. A subtle production detail that's easy to miss until something's deployed behind a gateway.

The reframe: I set out to remove a dependency and found the dependency was mostly wrapping a framework primitive I already had. Once configuration loads before anything renders, it stops being asynchronous — and every "is the config ready yet?" workaround you'd accumulated just evaporates.