Loading external configurations via http using APP_INITIALIZER
Tags: angular, tutorial, typescript
Published: 2022-03-20 11:13:08

Given that title, I don't think I need to explain. So let's get on with it.

### Fetching Configuration from server
Environment variables pollute the compiled source code, which does not allow for multiple server deployments. External configuration, allows multiple custom configuration for the same source code. The downside is, you have to maintain them manually.
![Remote Configuration in Angular](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pypsgz0e9u1heurv9tet.png)
 
Let's begin by creating the config json file with some keys:

> The full project is on [StackBlitz](https://stackblitz.com/edit/token-test)

```json
{
  "API": {
   "apiRoot": "http://localhost:8888/.netlify/functions"
  },
  "MyKey": "MyValue"
}
```

The end result in some component, is to be able to get the configuration as a property of a service, or as a static member.

```typescript
 // Component
constructor(private configService: ConfigService) {
  
}
ngOnInit(): void {
    const myValue = this.configService.Config.MyKey;
    // or
    const myStaticValue  = ConfigService.Config.MyKey;
}
```

### APP_INITIALZER token
In **AppModule**: (refer to [mysterious three tokens](https://garage.sekrab.com/posts/the-mystic-three-tokens-of-angular-app_bootstrap_listener-app_initializer-platform_initializer) post).

```typescript
@NgModule({
  imports: [BrowserModule, HttpClientModule, CommonModule],
  declarations: [AppComponent, HelloComponent],
  bootstrap: [AppComponent],
  providers: [
    {
      // TODO: create ConfigService and configFactory
      provide: APP_INITIALIZER,
      useFactory: configFactory,
      multi: true,
      deps: [ConfigService]
    },
  ],
})
export class AppModule {}
```
In a service file for `ConfigService`:

```typescript
export const configFactory = (config: ConfigService): (() => Observable<boolean>) => {
  return () => config.loadAppConfig();
};

@Injectable({
  providedIn: 'root',
})
export class ConfigService {
  constructor(private http: HttpClient) {
    
  }
  // retursn observable, right now just http.get
  loadAppConfig(): Observable<boolean> {
    return this.http.get(environment.configUrl).pipe(
      map((response) => {
        // do something to reflect into local model
        this.CreateConfig(response);
        return true;
      }),
      catchError((error) => {
        // if in error, set default fall back from environment
         this.CreateConfig(defaultConfig);
        return of(false);
      })
    );
  }
}
```
The `evironment.configUrl` in development would be the local file, ore remote server. Later will be elaborating more on strategy of how to handle the config file and location.

The `Config` model:

```typescript
export interface IConfig {
  API: {
    apiRoot: string;
  };
  MyKey: string;
}
```

The private method to cast configuration, should also return default configuration in case of failure. The extra configuration though does not have to match `IConfig`.

The default fallback config:

```typescript
import { environment } from '../enviornments/dev.env';

export const Config = {
  API: {
    apiRoot: environment.apiRoot,
  },
  MyKey: 'default value',
  ExtraKeys: 'wont harm',
};
```
Back to the service, the `CreateConfig` should only try to cast, then set to a public property. **This, later, is going to fail. But let's go on.**

```typescript
export class ConfigService {
  constructor(private http: HttpClient) {}

  private _createConfig(config: any): IConfig {
    // cast all keys as are
    const _config = { ...(<IConfig>config) };
    return _config;
  }
  // public property
  public Config: IConfig;

  loadAppConfig(): Observable<boolean> {
    return this.http.get(environment.configUrl).pipe(
      map((response) => {
        // set to public property
        this.Config = this._createConfig(response);
        return true;
      }),
      catchError((error) => {
        // if in error, return set fall back from Config
        this.Config = Config;
        return of(false);
      })
    );
  }
}
```
### The curious case of Router Initialization
The Router Module uses APP_INITIALIZE, as referenced in [master branch of Angular 13](https://github.com/angular/angular/blob/master/packages/router/src/router_module.ts#L631), and initialization functions are **run in parallel** according to [source code](https://github.com/angular/angular/blob/master/packages/core/src/application_init.ts#L138). Without digging deeper into navigation options, it is already an **open wound** that needs to be patched. **The sequence of events cannot be guaranteed** in a module that uses both configuration and Route modules. One is going to happen before the other.

Route guards and resolves are one example of routing happening sooner than initialization response. The extreme case I reached after multiple trials:

- The external configuration is remote, thus a bit slower than local
- Routing option `InitialNavigation` is set to `enabledBlocking`, according to Angular docs, this is required for SSR.

> A word of caution, leaving the `InitialNavigation` to its default "`enabledNonBlocking`" will produce unexpected results in the resolve service. Filtering out unready configuration to avoid "fallback" values, the benefit of "non blocking" is nullified. Read the code comments as you go along. 

So let's create an app routing module and add a router resolve with these extreme conditions.

```typescript
// the routing module
const routes: Routes = [
  {
    path: 'project',
    component: ProjectComponent,
    resolve: {
      // add a project resolve
      ready: ProjectResolve,
    },
  },
 // ...
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      // enabledBlocking for SSR, but also enabledNonBlocking is not as good as it sounds in this setup
      initialNavigation: 'enabledBlocking',
    }),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}
```
Import the `AppRoutingModule` into root `AppModule`, add a project component, and let's create the project resolve, that returns a Boolean.

```typescript
@Injectable({ providedIn: 'root' })
export class ProjectResolve implements Resolve<boolean> {
  // inject the service
  constructor(private configService: ConfigService) {}
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
    // log the value of the configuration here
    // if this is too soon, the result is undefined
    console.log('on resolve', this.configService.Config);
    
    return of(true);
  }
}
```
Running this on [Stackblitz](https://stackblitz.com/edit/token-test) and loading the app on `/project`, consoles "**undefined**." Which means, the **initial Route Resolve was faster than getting http config result**. The solution to that, if we see it backwards, should be like this:

> wait till this.configService.Config is ready

That translates to **RxJS observable**. So let me head to `ConfigService` and create an observable of an internal subject (much like [RxJS state management](https://garage.sekrab.com/posts/rxjs-based-angular-state-management)).

```typescript
// config service
export class ConfigService {
  constructor(private http: HttpClient) {}

  // keep track of config, initialize with fall back Config
  private config = new BehaviorSubject<IConfig>(Config as IConfig);
  config$: Observable<IConfig> = this.config.asObservable();

  private _createConfig(config: any): IConfig {
    // cast all keys as are
    const _config = { ...(<IConfig>config) };
    return _config;
  }

  loadAppConfig(): Observable<boolean> {
    return this.http.get(environment.configUrl).pipe(
      map((response) => {
        const config = this._createConfig(response);
       
        // here next
        this.config.next(config);
        return true;
      }),
      catchError((error) => {
        // if in error, return set fall back from Config
        this.config.next(Config);
        console.log(error);
        return of(false);
      })
    );
  }
}
```
In the resolve service, watching updates is not good enough, we need to **signal end of stream**, to return and move on. RxJS `take(1)` is usually recommended, but before we take 1, **we need to filter out configuration that is not ready yet**, otherwise, that "1" would be the fallback one. This, is why `enabledNonBlocking` is useless in this setup (I hope I'm clear, if not, let me know in the comments and I will try to clear that out).

```typescript
// in resolve, need to take 1 and return
// This is he first attempt
 return this.configService.config$.pipe(
  take(1),
  map(n => {
      if (n.MyKey === 'default') {
        // the first one will actually be the fallback
          return false;
      }
      return true;
  }));
    
// attempt two: filter before you take
return this.configService.config$.pipe(
  filter(n => n['somevalue to distinguish remote config'])
  take(1),
  map(n => {
      if (n.MyKey === 'default') {
          return false;
      }
      // it will be true for sure
      return true;
  }));
  
// last attempt, two in one:
return this.configService.config$.pipe(
  first(n => n['somevalue to distinguish remote config']
  map(n => {
      // always same value
      return true;
  }));
```

`isServed` is my new configuration property to "distinguish remote configuration" from fallback one. It's just a Boolean set to true in remote config. 

```json
// config json
{
  "isServed": true,
  "API": {
    "apiRoot": "http://localhost:8888/server/app"
  },
  "MyKey": "MyValue"
}
```
Add it to the config model, and to the default Config.
```typescript
// config model:
export interface IConfig {
  isServed: boolean;
  API: {
    apiRoot: string;
  };
  MyKey: string;
}

// the default Config with isServed: false
export const Config = {
  isServed: false,
  API: {
    apiRoot: environment.apiRoot,
  },
  MyKey: 'default value',
  ExtraKeys: 'wont harm',
};
```
The project resolve is ready

```typescript
@Injectable({ providedIn: 'root' })
export class ProjectResolve implements Resolve<boolean> {
  
  constructor(private configService: ConfigService) {}
  
  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> {
    
    // watch it until it's served
    return this.configService.config$.pipe(
      first((n) => n.isServed),
      map((n) => true)
    );
  }
}
```
The observable in the current setup shall produce two values, the first is `isServed` set to false. To read the configuration in a component:

```typescript
@Component({
  template: `Project page with resolve
  <p>
  {{ config$ | async | json}}
  </p>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProjectComponent implements OnInit {
  config$: Observable<IConfig>;

  constructor(private configService: ConfigService) {
  }
  
  ngOnInit(): void {
       this.config$ = this.configService.config$;
  }
}
```
A final touch to garnish, for off the track usage, we add static getter, that returns the value of the configuration:

```typescript
// config service

// make a static member
private static _config: IConfig;

// and a static getter with fallback
static get Config(): IConfig {
  return this._config || Config;
}

private _createConfig(config: any): IConfig {
  const _config = { ...(<IConfig>config) };
  // set static member
  ConfigService._config = _config;
  return _config;
}

// ...

// This can be used directly, for example  in template
{{ ConfigService.Config.isServed }}
```

### Pitfalls

1. If the remote configuration does not have all keys expected, they will be overwritten to "null". To overcome, extend the configuration, via shallow cloning. 

```typescript
private _createConfig(config: any): IConfig {
    // shallow extension of fallback
    const _config = {...Config, ...(<IConfig>config) };
   
    ConfigService._config = _config;
    return _config;
 }
```

2. The default `Config` may be mistaken for `ConfigService.Config`, if ever used, the default fallback value is in place. To fix that, a separation between the general Config, and remote Config fallback may be needed, or a little bit of attention. 

3. If the config file needed in Route Resolve or Guard fails to be served, we're blocked. Placing the config file on the same server, or a combination of RxJS operators, are possible solutions. 

4. The url of the config file, cannot be part of the configuration keys!

5. Remember to filter out config url in your HTTP interceptor, if you prefix urls with a value fed by configuration.

### Where to place the config file

The benefit aspired for is to have a production-specific configurations for every deployed version, ready to be adjusted for whatever prompt reason. As much as you would like to believe that touching production is taboo, there shall be times when the kitchen is on fire.

The question is, where to place configuration **during development**. 

1. Remote server. Can be an inhouse local server, or a staging server. 

2. Mock server, a nodejs local server that you run before starting Angular.

3. On a root folder, e.g. "configs", served via `angular.json assets`. 
Update: this will copy the file into production under the same url (localdata/config.json), if that is not your intention, remember to remove this entry from production assets in `angular.json`

Wherever you decide to place your configuration, remember to update respective environments.

```json
// add this to assets in angular.json
 "assets": [
  {
    "glob": "*.json",
    "input": "configs",
    "output": "/localdata"
  }
]

// now, every ./configs/*.json will be accessed in dev env as /localdata/*.json
```

### Inline, SSR, and other issues

There is another way to load configuration without HTTP, and that is to inject the JS file in the header of the index file. To accomplish that ...come back next week. 😴 

Thank you for tuning in. Let me know in the comments if I pressed any wrong buttons.

> This configuration setup is part of the [Cricket Seed](https://github.com/ayyash/cricket).

#### Resources

- [Github Issue Guards and Resolvers don't wait for APP_INITIALIZER](https://github.com/angular/universal/issues/1623#issuecomment-610671572)
- [Using absolute URLs for HTTP (data) requests on the server](https://angular.io/guide/universal#using-absolute-urls-for-http-data-requests-on-the-server)
- [RxJS Take](https://www.learnrxjs.io/learn-rxjs/operators/filtering/take)
- [Stackblitz](https://stackblitz.com/edit/token-test)