The same app runs on your laptop, on a test server and in production, and each place needs different settings: the API address, a feature flag, an analytics key. This lesson shows the safe ways to do it.
What you will learn
ApplicationConfig: where app-wide providers live- Environment files (build time) versus a runtime
config.json provideAppInitializer, which replaces the legacyAPP_INITIALIZER- The
Locationservice and path versus hash URLs
Application configuration
In a standalone app, bootstrapApplication(App, appConfig) receives one object with a providers array. That array is your app configuration: router, HTTP client, error handler and your own services all go there.
// File: src/app/app.config.ts (the shape you already know)
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideHttpClient()],
};
Everything in this lesson is just "more providers".
Option 1: environment files (build time)
Settings that are fixed when you build can live in environment files. Generate them with ng generate environments. You get:
src/environments/environment.ts(production values)src/environments/environment.development.ts(used byng serve)
// File: src/environments/environment.ts
export const environment = {
production: true,
apiUrl: 'https://api.example.com',
};
// File: use it anywhere
import { environment } from '../environments/environment';
const url = environment.apiUrl + '/posts';
The build swaps the file through fileReplacements in angular.json. The logic is simple: one import, different file per build.
Option 2: runtime config (one build, many servers)
With environment files you rebuild for every server. If you want one build that you can copy to test and production, load a JSON file when the app starts. Put config.json in the public folder and it is served as /config.json:
{ "apiUrl": "https://api.example.com", "featureBanner": true }
provideAppInitializer
An app initializer is a function that Angular runs before the app is shown. If it returns a Promise or an Observable, Angular waits for it. This is the perfect place to load config.json.
The old way was the APP_INITIALIZER token with multi: true. It still works, but it is deprecated. The modern way is provideAppInitializer:
// src/app/config/app-config.service.ts
import { Injectable, signal } from '@angular/core';
export interface RuntimeConfig {
apiUrl: string;
featureBanner: boolean;
}
@Injectable({ providedIn: 'root' })
export class AppConfigService {
private readonly state = signal<RuntimeConfig>({ apiUrl: 'http://localhost:3000', featureBanner: false });
readonly config = this.state.asReadonly();
async load(fetcher: () => Promise<RuntimeConfig> = () => fetch('/config.json').then((r) => r.json())) {
try {
this.state.set(await fetcher());
} catch {
// keep the safe defaults if the file is missing
}
}
}
// src/app/config/config.providers.ts
import { EnvironmentProviders, inject, provideAppInitializer } from '@angular/core';
import { AppConfigService } from './app-config.service';
export function provideRuntimeConfig(): EnvironmentProviders {
// the function runs in an injection context, so inject() works
return provideAppInitializer(() => inject(AppConfigService).load());
}
Add provideRuntimeConfig() to the providers array in app.config.ts. Because load() returns a Promise, the app waits for the config before routing starts.
Logic: defaults first, file overrides, failure keeps the defaults. Your app never starts half configured.
Compare with the legacy form so you can read old code:
// File: legacy, deprecated
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [AppConfigService],
useFactory: (config: AppConfigService) => () => config.load(),
},
]
Keep initializers fast. A slow one delays the first screen for every visitor.
Test it
// src/app/config/app-config.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideAppInitializer, inject } from '@angular/core';
import { AppConfigService } from './app-config.service';
describe('AppConfigService', () => {
it('uses defaults until loaded, then the file values', async () => {
const service = TestBed.inject(AppConfigService);
expect(service.config().featureBanner).toBe(false);
await service.load(async () => ({ apiUrl: 'https://api.test', featureBanner: true }));
expect(service.config().apiUrl).toBe('https://api.test');
expect(service.config().featureBanner).toBe(true);
});
it('keeps defaults when loading fails', async () => {
const service = TestBed.inject(AppConfigService);
await service.load(async () => {
throw new Error('missing');
});
expect(service.config().apiUrl).toBe('http://localhost:3000');
});
it('runs the initializer before the app is used', async () => {
let loaded = false;
TestBed.configureTestingModule({
providers: [
provideAppInitializer(() => {
inject(AppConfigService);
loaded = true;
}),
],
});
TestBed.inject(AppConfigService);
await TestBed.inject(AppConfigService).load(async () => ({ apiUrl: 'x', featureBanner: false }));
expect(loaded).toBe(true);
});
});
The Location service
Location (from @angular/common) talks to the browser address bar and history without the router:
// src/app/config/back-button.ts
import { Location } from '@angular/common';
import { Component, inject } from '@angular/core';
@Component({
selector: 'app-back-button',
template: `
<button type="button" (click)="back()">Back</button>
<small>Current path: {{ path }}</small>
`,
})
export class BackButton {
private readonly location = inject(Location);
path = this.location.path();
back() {
this.location.back();
}
}
Useful methods: back(), forward(), path(), go(path). In normal apps prefer the Router for navigation and use Location for "go back".
Path URLs versus hash URLs
The router has two location strategies:
- PathLocationStrategy (default): clean URLs like
/learn/angular. The server must returnindex.htmlfor unknown paths (see the troubleshooting lesson). - HashLocationStrategy: URLs like
/#/learn/angular. Works on any static host with no server rules, but looks older and is poor for SEO.
// File: enable hash URLs
provideRouter(routes, withHashLocation())
If the app lives in a sub-folder, set the base with <base href="/my-app/"> in index.html, or provide APP_BASE_HREF.
Common mistakes
- Putting secrets in environment files.
- Awaiting a slow request in an initializer and blocking the whole app.
- Forgetting that
config.jsoncan be cached by the browser or CDN. Use a short cache time for it. - Using
HashLocationStrategyonly to avoid fixing the server rule, then losing clean URLs.
Try it yourself
- Add
config.jsontopublicand showapiUrlin a component throughAppConfigService. - Change the file and reload without rebuilding.
- Switch the router to hash URLs and see how the address bar changes.
Quick check
- Why do environment files need a rebuild per server?
- What replaces
APP_INITIALIZER? - When would you choose hash URLs?
Next, continue with Angular Error Handling: Global ErrorHandler, Custom Handlers and HTTP Errors.