API-less Prototyping with Angular

gc_psk

Giancarlo Buomprisco

Posted on October 28, 2019

API-less Prototyping with Angular

Start prototyping with minimal changes to your application at integration time with Angular Injectables

This article was originally published on Bits and Pieces by Giancarlo Buomprisco

I am one of those developers who is usually fairly impatient when the Product Manager unveils the new product’s features that I can finally start working on.

After the sprint planning, I ask the dreaded question:

… is the API ready yet?

… and the answer is often times negative. Sometimes your backend colleagues are busier with other things, and can’t provide even a mock endpoint. No fear, though!

Provided you have a viable DTO interface your back-end colleague agrees with, you can start prototyping the new features with minimal changes to your application at integration time with the real API endpoints.

In order to minimize the changes needed during the integration, we will leverage the power of Angular’s Injectables and will work on both the API clients at the same time:

  • one is the mocked version, which we will call MockPriceApiService

  • the other is the actual service that will be shipped with the application when the API will be ready to be consumed, which we call PriceApiService

  • the two classes will implement an interface to establish an API contract between the two


A quick tip: Use Bit (Github) to share, reuse and update your Angular components across apps**. Bit track and bundles reusable components in your projects, and exports them encapsulated with their dependencies, compilers and everything else. Components can then be installed with package managers, and even updated right from any new project. Give it a try.


Yet again (!), I will build a minimal live cryptocurrency application that will work with both mocked prices and with an actual service that will pull prices from Coincap.

If you’re impatient or just TLDR; you can see the final result on Stackblitz!

Final ResultFinal Result

The API Interface

First thing, we want to establish which API the two classes will share. So, we build an interface called ApiService

import { Observable } from 'rxjs';

export interface PriceApi {
  getPrice(currency: string): Observable<string>;
  unsubscribe(): void;
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the services that will implement this interface will only need to provide two public methods:

  • one to subscribe to a stream of prices

  • the other to unsubscribe the stream.

Mock Service

Let’s start by creating the mock service. As we have said before, we only need to implement two methods to respect the contract. In short, what we want to do is:

  • get a stream of random numbers (prices), and we will base this off a set of base prices we use as a reference to make the numbers close to the real ones

  • unsubscribe said stream

The app we will create will only support 3 currencies: Bitcoin, Litecoin and Ethereum.

const BASE_PRICES = {
  bitcoin: 11000,
  litecoin: 130,
  ethereum: 300
};

const randomize = (price: number) => {
  return (Math.random() * 2) + price;
};

@Injectable()
export class MockPriceApiService implements PriceApi {
  static latency = 250;
  private unsubsciptions$ = new Subject();

  public getPrice(currency: string): Observable<string> {
    return timer(0, MockPriceApiService.latency).pipe(
      delay(1000),
      map(() => BASE_PRICES[currency]),
      map((price: number) => randomize(price)),
      map((price: number) => price.toFixed(5)),
      takeUntil(this.unsubsciptions$),
    )
  }

  public unsubscribe() {
    this.unsubsciptions$.next();
  }
}
Enter fullscreen mode Exit fullscreen mode

Let’s explain the method getPrice:

  • we receive a parameter called currency which represents the name of the asset subscribed

  • we create a timer that will emit an event every 250ms

  • we delay the emitted observable by 1 second to mimic a real network latency

  • we take the BASE_PRICE of the selected currency and return a number that gets randomized at each emission

  • we unsubscribe the stream when the subject unsubscriptions$ emits

Real Live-Prices Service

As we said before, your team may take a while before a real API is provided to you to implement in your front-end.

If your team already agreed on the interface, though, there is nothing stopping you from implementing the API beforehand and build the real service alongside the mock service.

In order to complete our example, we will build the real service and take prices from Coincap’s Service (thank you again!) and use a WebSocket to stream prices to our client.

The two public methods will:

  • create a WebSocket connection and stream prices using an Observable, respecting the contract in the interface

  • close the connection when the method unsubscribe is called. As a result, the prices stream will stop emitting prices.

const WEB_SOCKET_ENDPOINT = 'wss://ws.coincap.io/prices/';

@Injectable()
export class PriceApiService implements PriceApi {
  private webSocket: WebSocket;

  public getPrice(currency: string): Observable<string> {
    return this.connectToPriceStream(currency);
  }

  public unsubscribe() {
    this.webSocket.close();
  }

  private connectToPriceStream(asset: string): Observable<string> {
    this.createConnection(asset);

    return new Observable(observer => {
      const webSocket = this.webSocket;

      webSocket.onmessage = (msg: MessageEvent) => {
        const data = JSON.parse(msg.data);
        observer.next(data[asset]);
      };

      return {
        unsubscribe(): void {
          webSocket.close();
        }
      };
    });
  }

  private createConnection(asset: string) {
    if (this.webSocket) {
      this.webSocket.close();
    }

    this.webSocket = new WebSocket(
      WEB_SOCKET_ENDPOINT + `?assets=${asset}`
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Consuming the Service’s Data

Now, let’s show how to actually consume the data streamed by any of the two services we created.

Let’s create a component that injects PriceApiService:

  • we create a property to which we assign the stream, which we call price$

  • when the user changes cryptocurrency, we call the method unsubscribe, unless the currency has never been selected

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  public price$: Observable<string | undefined>;
  public currency$ = new BehaviorSubject<string | undefined>(undefined);

  constructor(private api: PriceApiService) { }

  ngOnInit() {
    this.price$ = this.currency$.pipe(
      mergeMap((currency: string | undefined) => {
        return currency ? this.api.getPrice(currency) : of(undefined);
      })
    );
  }

  onCryptoSelected(currency: string) {
    if (this.currency$.value) {
      this.api.unsubscribe();
    }

    this.currency$.next(currency);
  }
}
Enter fullscreen mode Exit fullscreen mode

And now on to the template:

  • we display the price

  • if the price is still undefined, but currency has been selected, it means a subscription is still in progress: as a result, we show to the user a loading message

  • if the currency hasn’t been selected, we show an empty message

<div class='mt-5'>
  <crypto-selector (selected)="onCryptoSelected($event)"></crypto-selector>

  <div class='price'>
    {{ price$ | async }}
  </div>

  <ng-container *ngIf="(price$ | async) === undefined && (currency$ | async)">
    <div class='alert alert-info mt-2'>
      Awaiting for Price...
    </div>
  </ng-container>

  <ng-container *ngIf="!(currency$ | async)">
    <div class='alert alert-warning mt-2'>
      No Crypto Subscribed Yet
    </div>
  </ng-container>
</div>
Enter fullscreen mode Exit fullscreen mode

Switching Services using Dependency Injection

Here is the magic part. Thanks to Angular’s Dependency Injection, we can provide which service we want to use by passing to it the type and the actual class.

As you can see below, we instruct the DI:

  • when the user injects PriceApiService

  • use the following class: if useMocks is truthy, use MockPriceApiService, otherwise, use PriceApiService.

// in a real app could be environment.useMocks ?
const useMocks = true;

@NgModule({
  imports: [BrowserModule, CommonModule],
  declarations: [AppComponent, CryptoSelectorComponent],
  bootstrap: [AppComponent],
  providers: [
    {
      provide: PriceApiService,
      useClass: useMocks ? MockPriceApiService : PriceApiService
    }
  ]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

Of course, while your real endpoints are still being built, your prototype will continue using MockPriceApiService for as long as it’s needed.

Once your team is ready to switch to the real endpoints, all you’ll have to do is switch the environment variable to false (which could be set in your CI) and your app will automatically work with the real endpoints!

Angular’s DI

Angular’s Dependency Injection is very powerful.

In addition to useClass, you can also use:

  • useValue: for example, if you want to pass a number, a string, etc.

  • useFactory: for example, when you want to pass a function and make use of the DI to inject dependencies

And you can also use InjectionToken to pass different values other than classes.

For a complete reference about dependency injection, I’d suggest you read the full documentation at Angular.io.

Result

You can see the final result on Stackblitz!

Final Words

The Dependency Injection is a powerful tool, and can be used in a variety of useful ways:

  • prototyping

  • feature flags

  • mocking services for tests

  • general configuration

Learning how to master it can improve your Angular projects’ architecture and can allow patterns you didn’t know where even possible.


If you need any clarifications, or if you think something is unclear or wrong, please leave a comment!

If you enjoyed this article, follow me on Medium or Twitter for more articles about Angular, RxJS, Typescript and more!

💖 💪 🙅 🚩
gc_psk
Giancarlo Buomprisco

Posted on October 28, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related