How to make Web API endpoint’s base URL, configurable in Angular2 Nswag clients

For developers working on Angular2 + Web API projects, Swagger is a familiar name. Swagger is a nice tool to expose and provide a documentation and a UI for testing Web API controllers and methods (Generally speaking for RESTful APIs).

swagger

NSwag is another set of tools to create client classes of RESTful APIs. It’s actually a code generation tool whereby we can create Web API client classes (including Angular2 services in Typescript). By Web API clients I mean the classes that call Web API methods through HTTP and return their results. NSwag Studio is one of the handy tools we can use to generate the client classes. For more information about NSwag and how it works click here. As learning about Swagger and NSwag is out of this article’s scope.

The problem I’m going to address in this article though, is: “How to get NSwag clients to read their Web API endpoint from JSON config files.”.

Remember that if your Angular2 app is hosted on the same address as your Web API endpoint, then this article would not be relevant. As in these cases we don’t need to set the Web API endpoint. In this topic we’re assuming that the Angular2 app is going to call Web API methods from a separate end point (which is the case quite often, in a well-architectured Angular app).

If you take a look into the NSwag studio generated classes, you will see something like this:

export const API_BASE_URL = new OpaqueToken(‘API_BASE_URL’);

export interface IAuthClient {
authenticate(loginInfo: LoginInfoViewModel): Observable;
}

@Injectable()
export class AuthClient extends ApiClientBase implements IAuthClient {
private http: Http = null;
private baseUrl: string = undefined;
protected jsonParseReviver: (key: string, value: any) => any = undefined;

constructor(@Inject(Http) http: Http, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
super();
this.http = http;
this.baseUrl = baseUrl ? baseUrl : “”;
}

authenticate(loginInfo: LoginInfoViewModel): Observable {
let url_ = this.baseUrl + “/api/Auth/Authenticate”;

const content_ = JSON.stringify(loginInfo ? loginInfo.toJS() : null);

return this.http.request(url_, this.transformOptions({
body: content_,
method: “post”,
headers: new Headers({
“Content-Type”: “application/json; charset=UTF-8”,
“Accept”: “application/json; charset=UTF-8”
})
})).map((response) => {
return this.transformResult(url_, response, (response) => this.processAuthenticate(response));
}).catch((response: any, caught: any) => {

As you see in the code (notice the highlighted ones in particular), it’s using a constant named “API_BASE_URL” , which is being injected into the client’s constructor to set the Web API endpoint base url. If there is no provider for the API_BASE_URL, then it returns empty string which results in calling the Web API methods from the same url as Angular2 app is being hosted on.

Defining a provider for API_BaseURL, would be as easy is adding the highlighted code to the app module, provided that we want to simply hardcode the base url.

import { API_BASE_URL } from ‘../../nswagclients’;

@NgModule({
imports: [ …],
providers: [
{
provide: API_BASE_URL,
useFactory: () => {
              return ‘http://mywebapiurl.com’;
       }
}
],

})
export class AppModule {
}

Hard-coding a configuration though is not what we want in a real world project. We want to be able to change our configs without any need to rebuild our project. What we want is to have a simple JSON file, set the endpoint and refresh the browser to take effect.I’ve seen many JavaScript developers who don’t care about hard-coding such things and tend to fix these problems through task runners (such as Gulp and Grunt). Please don’t do that! Always go No! No! to hard-coding stuff, whether you’re coding in Java or C# that compiles the code into binary files, or in JavaScript/Typescript that goes plain text.

Once we want to take the Base Url out of the code, we face a serious problem. Our code needs to read configs from another file which is not a part of our code. You got the problem? That’s it!, we need to make an asynchronous call to the JSON file through HTTP, load the JSON file’s content and then get the API_BASE_URL‘s provider to return the value read from the JSON. It may seem easy thing to do but it’s not if you don’t know how to do it right. The main problem is that your provider’s factory method returns the value before the JSON config file is loaded.

To solve this problem we can use an Angular2 build-in provider named APP_INITIALIZER. This provider, simply runs the code we need to run before the Angular app gets started. So, in this case we’re going to call a method from a class that loads the JSON file and then sets a static field in the class, so that we can return the value in the API_BASE_URL’s factory.

import { API_BASE_URL } from ‘../../nswagclients’;
import { WebApiEndpointConfigService } from ‘../../services/web-api-endpoint-config.service’;
import { NgModule, APP_INITIALIZER } from “@angular/core”;

import { AppConfig } from ‘../../app-config’;

@NgModule({
imports: [ …],
declarations: […],
exports: […],
providers: [
WebApiEndpointConfigService,
{
    provide:APP_INITIALIZER,
    useFactory:() => ()=>
    {
let appConfig= ReflectiveInjector.resolveAndCreate([AppConfig]).get(AppConfig);

        let promise= appConfig.load().toPromise();
        return promise;
    },
    multi:true
},
{
    provide: API_BASE_URL,
    useFactory: () => {
         return AppConfig.webApiEndpointUrl;
    }
}
],
bootstrap:[]
})
export class AppModule {
}

As you see, in the code above, we’re using a service called WebApiEndpointConfigService, the code below shows how it is defined:

import { Injectable } from ‘@angular/core’;
import { Observable } from ‘rxjs’;
import { Http, Response } from ‘@angular/http’;
import { environment } from ‘../../environments/environment’;

@Injectable()
export class WebApiEndpointConfigService {
private _config: any = null;
private _env: string;
constructor(private http: Http) {
}
public load() {
var env = environment.production ? ‘production’ : ‘development’;
this._env = env;
let envConfigFilePath = ‘../../assets/config/’ + env + ‘.json’;
return this.http.get(envConfigFilePath)
.map(res => res.json())
.map((configData) => {
this._config = configData;
return configData;
}).catch(
error => {
console.error(error);
return Observable.throw(error.json().error || ‘Server error’);
});

}
getConfig() {
if (this._config === null) {
return this.load().map(config => { if (config.enableDebug) { console.log(‘Web Api         Endpoint:’ + config.webApiEndpoint); } return config; });
}
else {
return Observable.create((observer) => {
if (this._config.enableDebug) {
console.log(‘Web Api Endpoint:’ + this._config.webApiEndpoint);
}
observer.next(this._config);
});
}
}
}

And here’s the content of AppConfig class used in the provider code:

import { WebApiEndpointConfigService } from ‘./services/web-api-endpoint-config.service’;
import { HttpModule, Http, XHRBackend, ConnectionBackend, BrowserXhr, ResponseOptions, XSRFStrategy, BaseResponseOptions, CookieXSRFStrategy, RequestOptions, BaseRequestOptions} from ‘@angular/http’;
import {ReflectiveInjector, Injectable} from ‘@angular/core’;
@Injectable()
class MyCookieXSRFStrategy extends CookieXSRFStrategy {
constructor(){
super(”,”);
}
}

@Injectable()
export class AppConfig{
public static webApiEndpointUrl: string;
private static configLoaded:Boolean = false;

constructor() {
}

load(){
if(AppConfig.configLoaded)
{
return;
}
let injector: any = ReflectiveInjector.resolveAndCreate([
WebApiEndpointConfigService,
Http, BrowserXhr,
{ provide: ConnectionBackend, useClass: XHRBackend },
{ provide: ResponseOptions, useClass: BaseResponseOptions },
{ provide: XSRFStrategy, useClass: MyCookieXSRFStrategy },
{ provide: RequestOptions, useClass: BaseRequestOptions }
]);
let configService: WebApiEndpointConfigService =         injector.get(WebApiEndpointConfigService);
return configService.getConfig().map(config => {
AppConfig.webApiEndpointUrl = config.webApiEndpoint;
AppConfig.configLoaded = true;
return config;
});
}

}

Remember that the reason why we had to use ReflectiveInjector in Appconfig class is that at the time of running this code, there is no provider to inject the WebApiEndpointConfigService. So we have to inject the service and the whole chain of its dependencies.

And here is the last piece of the puzzle which is our JSON config file:

{
“webApiEndpoint”: “http://myWebAPIEndpoint.com” ,
“enableDebug”: true
}

So let’s briefly review what we’ve done to make the configurable endpoint happen:

  1. We made 2 config filesdevelopment.json or production.json , each of which representing the config for development and production environments.
  2. We defined a service that loads the JSON config file from the current Angular2 app’s host. We’re assuming that you’re using Angular CLI structure for your NG2 app. So as you see, we’re considering environment variables to load the pertinent config file which could be either development.json or production.json
  3. We made a class named AppConfig that holds the endpoint base url in a static variable and a load method that calls the service defined in step 2 and sets the pertinent static variable (webApiEndpointUrl) to the read value.
  4. In our AppModule, an APP_INITIALIZER provider was added that returns a Promise from the load method, defined in step 3 as a result of its factory method. That’s the key thing to run an asynchronous code in application start, and wait for the result before starting the app.
  5. Finally, another provider was added to the AppModule, to enable the API_BASE_URL injection to the NSwag code. The factory method of API_BASE_URL, simply returns the value of the static field defined in the step  4.

I spent a few days to come up with this solution to get rid of hard-coded base url for RESTful services. I hope this article saves time and effort for all fellow Angular2 developers.

Enjoy coding fellas!

Advertisements

2 thoughts on “How to make Web API endpoint’s base URL, configurable in Angular2 Nswag clients

  1. Hey, When doing tests I get “Cannot read property ‘SomeProperty’ of null”
    Do we need to call the load function manually somehow or how to establish the config in testing?

    Like

    1. Hey, what kind of test you mean? if it’s Jasmine test you shouldn’t be depending on any config as it’s a unit test, so you need to use a mock service rather than the real one.if it’s like protractor E2E test then it’s the same as what mentioned in the post. If none of them is the case please elaborate it.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s