
Intermediate workshop
Nx for Scalable Architecture
Master Nx to enforce architecture, speed up your development workflow and improve code quality
Learn more
Ever wanted to deploy the same Angular app for different clients—each with unique themes, API endpoints, or feature flags—all from a single build artifact? This is the holy grail of dynamic configuration. But achieving it without sacrificing developer experience (DX) or Server-Side Rendering (SSR) capabilities can be tricky.
One issue when creating these apps, is having to wait for that configuration to be loaded. This way the application won't be rendered until the configuration is available. On CSR apps, we can start fetching the configuration file directly in the index.htmlPromiseprovideAppInitializer
For example, our index.html
And our app.config.ts
import { provideAppInitializer } from "@angular/core";
import { appConfig } from "./app.config";
declare global {
interface Window {
loadConfigPromise: Promise
This way, the configuration is fetched directly in the index.html
We can't use injection tokens with a static value that comes from the configuration file. We cannot have that asynchronous behavior on the providers. For more read this issue https://github.com/angular/angular/issues/23279
If we go with this approach, every time we need to use the ConfigService
@Injectable({ providedIn: 'root'})
export class ConfigService {
#config = signal
Let's optimize this! We will move the logic to the main.tsprovideAppInitializer
We would do something like this:
// main.ts
window["loadConfigPromise"] // we still get the promise from the index.html file
.then((response) => response.json())
.then(async (config) => {
// 1. We can even fetch additional, user-specific data here!
const appData = await loadAppData(config); // Uses cookies to load data!
// 2. Build our application config dynamically! We can add providers dynamically here!
const config = await buildAppConfig({ config, appData });
// 3. Bootstrap the app ONLY after everything is ready
return bootstrapApplication(AppComponent, config);
})
.catch((err) => console.error(err));
We don't need to use a ConfigServiceInjectionTokensCONFIG_TOKENInjectionToken
Also, we can have more functions that load data for example loadAppData
Our app.config.ts
export const CONFIG_TOKEN = new InjectionToken
The DashboardMenuService example would look like this:
no async behavior for the config
no need for async keyword on the
loadMenu
@Injectable({ providedIn: 'root'})
export class DashboardMenuService {
#config = inject(CONFIG_TOKEN);
loadMenu() {
const url = `/api/${this.#config.menuAPIUrl}`
this.http.get(url).pipe(take(1)).subscribe(
x => /* do something */
)
}
}
That is great! We now have great DX and we have optimized the process of loading the configuration.
Now, let's see how we can migrate this approach to Server-Side Rendering (SSR).
In our server.ts
// server.ts
app.use("/**", async (req, res, next) => {
angularApp
.handle(req)
.then((response) => (response ? writeResponseToNodeResponse(response, res) : next()))
.catch(next);
});
And in main.server.ts
// main.server.ts
import { config } from "./app.config.server";
import { AppComponent } from "./app/app.component";
const bootstrap = () => bootstrapApplication(AppComponent, config);
export default bootstrap;
What's the issue here?
We can't really load our config and app data here. In the server.tsprovideAppInitializerapp.config.server.ts
Inside the provideAppInitializerREQUESTREQUEST_CONTEXTloadAppData
This requires a lot of changes to the codebase, because everything now would have to be async and we would have to check for the value of the ConfigService to be available.
In v20.3 Angular fixed a security issue! But it also introduced a feature!
Copied from the PR: https://github.com/angular/angular-cli/pull/31108
This commit introduces a number of changes to the server bootstrapping process to make it more robust and less error-prone, especially for concurrent requests.
Previously, the server rendering process relied on a module-level global platform injector. This could lead to issues in server-side rendering environments where multiple requests are processed concurrently, as they could inadvertently share or overwrite the global injector state.
The new approach introduces a BootstrapContext that is passed to the bootstrapApplication function. This context provides a platform reference that is scoped to the individual request, ensuring that each server-side render has an isolated platform injector. This prevents state leakage between concurrent requests and makes the overall process more reliable.
BREAKING CHANGE: The server-side bootstrapping process has been changed to eliminate the reliance on a global platform injector.
Before: const bootstrap = () => bootstrapApplication(AppComponent, config);
After: const bootstrap = (context: BootstrapContext) => bootstrapApplication(AppComponent, config, context);
How does this help us?
BootstrapContextplatformRef: PlatformRefinjector: Injector
This enables us to inject data that are request specific.
When updating to Angular v20.3, we need to update our main.server.tsBootstrapContext
// main.server.ts
- const bootstrap = () => bootstrapApplication(AppComponent, config);
+ const bootstrap = (context: BootstrapContext) => bootstrapApplication(AppComponent, config, context);
export default bootstrap;
Let's make our application config providers dynamic!
// app.config.server.ts
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { buildAppConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
// we can make serverConfig dynamic too, by converting it to a function accepting configs
// but we will keep it static as an example
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering(withRoutes(serverRoutes))],
};
// our server config builder, now accepts config and app data, and passed it to the dynamic app config
export const serverConfigBuilder = async (
{config, appData}: {config: SomeConfigType, appData: SomeAppDataType}
): Promise
With this change, we don't need to refactor any application code, we can just update our main.server.tsapp.config.server.ts
NOTE: BootstrapContext has been backported also to:
v19.2.6
v18.2.21
That's it! We have a dynamic application config that is scoped to the individual request, and we can inject providers that are request specific without dropping DX.
Let us know what you think! Thanks for reading!

Intermediate workshop
Master Nx to enforce architecture, speed up your development workflow and improve code quality
Learn more
Accessibility doesn’t have to be hard. Follow a comic-style, hands-on journey building an accessible day selector with Angular Aria, learning comboboxes, listboxes, and real screen reader behavior along the way.

Implement incremental hydration in a real-world Angular app - Basic setup, hydration triggers and important considerations for a seamless integration.

Let's dive deeper into why incremental hydration in Angular matters so much for performance and user experience, focusing on its influence on Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS).

Incremental hydration is a groundbreaking new feature in Angular that heavily impacts critical performance metrics like INP and LCP, while also reducing the effort required to maintain CLS at low levels.

PushBased's 2024 recap: 43 talks, NgGlühwein conference, RxAngular updates, and more. A year of growth, innovation, and community! Read our journey. 🚀

Think of an AST as the secret blueprint, the hidden structure that reveals the true meaning of your code – way more than just the lines you see on the screen. Let's dive in and demystify these ASTs, and I promise you'll see your code in a whole new light.