In angular 18, I need to set API URL after the app initialisation(because for obvious reason, fetching of URL from a json file). However it is not setting. In fact InjectionToken
has set before APP_INITIALIZER
even though it was added later in providers
array in app.config.ts
Reference: https://angular.dev/api/core/APP_INITIALIZER?tab=usage-notes
Here is theStackblitz
expecting to set API_URL_TOKEN
to url value and use it further for all the API calls. store in localStorage and assign.
private apiUrl = inject(API_URL_TOKEN);
loginUser(data: { username: string; password: string }) {
return this.http.post(`${this.apiUrl}/auth`, data).pipe(
tap((res) => {
if (res.success) {
this.addToken(res.data);
}
})
);
}
Here is what I tried
bootstrapApplication(AppComponent, appConfig)
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch(), withInterceptors([authInterceptor, errorInterceptor])),
provideAppInitializer(),
provideApiUrl(),
provideRouter(routes),
provideAnimationsAsync(),
provideCharts(withDefaultRegisterables()),
],
};
const CONFIG_URL = '/config.json';
function appInitializer(http: HttpClient, storageService: StorageService) {
return async () => {
try {
const config = await firstValueFrom(http.get<{apiUrl: string}>(CONFIG_URL));
storageService.apiUrl = config.apiUrl;
console.log('Config loaded successfully');
} catch (error) {
console.error('Error loading configuration:', error);
throw error;
}
};
}
export function provideAppInitializer(): Provider {
return {
provide: APP_INITIALIZER,
useFactory: appInitializer,
deps: [HttpClient, StorageService],
multi: true,
};
}
export const API_URL_TOKEN = new InjectionToken<string>('API_URL_TOKEN');
function apiUrlFactory(storageService: StorageService): string {
const apiUrl = storageService.apiUrl;
if (apiUrl) {
return apiUrl;
}
throw new Error('API URL not found in configuration');
}
export function provideApiUrl(): Provider {
return {
provide: API_URL_TOKEN,
useFactory: apiUrlFactory,
deps: [StorageService],
multi: false,
};
}
/* another way */
export const API_URL_TOKEN = new InjectionToken<string>('API_URL_TOKEN');
export function provideApiUrl(): Provider {
return {
provide: API_URL_TOKEN,
useValue: (storageService: StorageService) => storageService.getApiUrl,
deps: [StorageService],
};
}
import { inject, Injectable, InjectionToken } from '@angular/core';
const LOCAL_STORAGE = new InjectionToken<Storage>('Browser Storage', {
providedIn: 'root',
factory: () => localStorage,
});
const API_URL = 'apiUrl';
@Injectable({
providedIn: 'root',
})
export class StorageService {
private readonly storage = inject<Storage>(LOCAL_STORAGE);
get(key: string) {
return this.storage.getItem(key);
}
set(key: string, value: string): void {
this.storage.setItem(key, value);
}
remove(key: string): void {
return this.storage.removeItem(key);
}
clear() {
return this.storage.clear();
}
get apiUrl(): string | null {
return this.storage.getItem(API_URL) || null;
}
set apiUrl(url: string) {
this.storage.setItem(API_URL, url);
}
}
In second case its clear that, it is before the initialiser
The below github issue helped me understand what went wrong.
HTTP_INTERCEPTOR does not wait for APP_INITIALLIZER
Since your interceptor uses HttpClient
, this in turn calls the interceptor hence you are getting this issue. The solution to this problem is to use HttpBackend
whose requests are not intercepted by the interceptor, this will solve your problem. The input should be passed as HttpRequest
function appInitializer(http: HttpBackend, storageService: StorageService) {
return () => {
return http.handle(new HttpRequest('GET', CONFIG_URL)).pipe(
map((config: any) => {
storageService.apiUrl = config.apiUrl;
console.log('Config loaded successfully');
}),
catchError((error: any) => {
console.error('Error loading configuration:', error);
throw error;
})
);
};
}
export function provideAppInitializer(): Provider {
return {
provide: APP_INITIALIZER,
useFactory: appInitializer,
deps: [HttpBackend, StorageService],
multi: true,
};
}
Place your config.json
in an assets folder,to fetch the data. Then configure this in the angular.json's assets folder, so that it will be discoverable. As seen in the stackblitz.
...
},
"options": {
"assets": ["src/assets"],
"index": "src/index.html",
"browser": "src/main.ts",
...
You can get rid of the async await
and do this with pure rxjs. we can use map
to assign the values to storage and use catchError
to catch any exceptions.
function appInitializer(http: HttpClient, storageService: StorageService) {
return () => {
return http.get<XiConfig>(CONFIG_URL).pipe(
map((config: any) => {
storageService.apiUrl = config.apiUrl;
console.log('Config loaded successfully');
}),
catchError((error: any) => {
console.error('Error loading configuration:', error);
throw error;
})
);
};
}
One more Thing is you need to import the app config to bootstrap application.
bootstrapApplication(App, appConfig);
Then finally access this on the component.
@Component({
selector: 'app-root',
standalone: true,
template: `
<h1>Hello from {{ name }}!</h1>
<a target="_blank" href="https://angular.dev/overview">
Learn more about Angular
</a>
`,
})
export class App {
name = 'Angular';
constructor(@Inject(API_URL_TOKEN) private urlToken: string) {
console.log(urlToken);
}
}
import { Component, Inject } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';
import { appConfig } from './app/app.config';
import { API_URL_TOKEN } from './app/app-url-token';
@Component({
selector: 'app-root',
standalone: true,
template: `
<h1>Hello from {{ name }}!</h1>
<a target="_blank" href="https://angular.dev/overview">
Learn more about Angular
</a>
`,
})
export class App {
name = 'Angular';
constructor(@Inject(API_URL_TOKEN) private urlToken: string) {
console.log(urlToken);
}
}
bootstrapApplication(App, appConfig);
import { ApplicationConfig } from '@angular/core';
import {
provideHttpClient,
withFetch,
withInterceptors,
} from '@angular/common/http';
import { provideAppInitializer } from './app-initializer';
import { provideApiUrl } from './app-url-token';
//import { provideRouter } from '@angular/router';
//import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
//import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
//import { routes } from './app.routes';
//import { authInterceptor } from './interceptors/auth.interceptor';
//import { errorInterceptor } from './interceptors/error.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch(), withInterceptors([])),
provideAppInitializer(),
provideApiUrl(),
//provideRouter(routes),
//provideAnimationsAsync(),
//provideCharts(withDefaultRegisterables()),
],
};
import { InjectionToken, Provider } from '@angular/core';
import { StorageService } from './storage.service';
export const API_URL_TOKEN = new InjectionToken<string>('API_URL_TOKEN');
function apiUrlFactory(storageService: StorageService): any {
console.log(storageService.apiUrl);
const apiUrl = storageService.apiUrl;
if (apiUrl) {
console.log('set apiUrl');
return apiUrl;
}
throw new Error('API URL not found in configuration');
}
/*
export const API_URL_TOKEN = new InjectionToken<string>('API_URL_TOKEN');
export function provideApiUrl(): Provider {
return {
provide: API_URL_TOKEN,
useValue: (storageService: StorageService) => storageService.getApiUrl,
deps: [StorageService],
};
}
*/
export function provideApiUrl(): Provider {
return {
provide: API_URL_TOKEN,
useFactory: apiUrlFactory,
deps: [StorageService],
multi: false,
};
}
import { HttpBackend, HttpClient, HttpRequest } from '@angular/common/http';
import { APP_INITIALIZER, Provider } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { StorageService } from './storage.service';
import { map, catchError } from 'rxjs';
interface XiConfig {
apiUrl: string;
}
const CONFIG_URL = '/assets/config.json';
function appInitializer(http: HttpBackend, storageService: StorageService) {
return () => {
return http.handle(new HttpRequest('GET', CONFIG_URL)).pipe(
map((config: any) => {
storageService.apiUrl = config.apiUrl;
console.log('Config loaded successfully');
}),
catchError((error: any) => {
console.error('Error loading configuration:', error);
throw error;
})
);
};
}
export function provideAppInitializer(): Provider {
return {
provide: APP_INITIALIZER,
useFactory: appInitializer,
deps: [HttpBackend, StorageService],
multi: true,
};
}