DEV Community

Van Zelleb
Van Zelleb

Posted on • Updated on

A pattern for managing API calls

The need for alternative API calls

In the process of coding a stock portfolio app prototype I came across the problem to handle the responses of various free stock information APIs, such as IEXCloud or Finnhub. Those information providers either limit the content of their free-tier API responses or simply do not have the stock prices for all markets worldwide available. However, by combining API responses from different such providers it is possible to fill those gaps in most cases. So I needed to find a way to call a secondary (or more) APIs after an unsuccessful first attempt. The below pattern intends to cover just that with some basic error handling included.

Separating the API logic

My starting point was an article from Francesco Zuppichini. It gave me the idea to separate the core of the API logic into a separate api.js file. That allows for making API calls from any place in my application, by importing the file where needed like this: import * as API from "./api"

Handling API responses (and errors)

In a successful scenario the API response to a query for the latest stock price is a JSON object that contains various key:value pairs. The keys that are present in the response object differ from provider to provider. To be able to use the same logic for any kind of API call and provider, I needed to find a way to simply return the API response without worrying about its content.

On top of that the function I was looking for had to handle API errors as well. Errors could occur when trying to query for a non-existent stock symbol. In such cases, the API providers do not return a JSON object, but a simple response text such as Unknown symbol. Through trial and error I realised that some API providers use 40X HTTP response codes to flag issues, while others use code 200 (ok) no matter what information is returned. That meant that I had no way to distinguish successful API calls from failed ones by using code like this:
if (response.ok) { //handle successful API call...}
else { // handle API error messages }

My solution to this problem was to simply try to parse each API response as JSON and to treat parsing errors as a sign for the presence of an API error message. Here is the code for this generic function that is located in my api.js file:

// api.js 

export async function getApiResponse(url, params) {
  url = new URL(url);
  Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
  return new Promise(async function(resolve, reject) {
    let text = null;
    try {
      const res = await fetch(url);
      // by reading the response as text we can retrieve API error messages
      text = await res.text();
      // this will cause an error when the response is not a JSON string
      let json = JSON.parse(text);
      // resolve the promise and return the API response
      resolve(json);
    } catch (e) {
      // in case of any error this returns either null or the API error response text, e.g. "Unknown symbol"
      reject(text);
    }
  });
}

Defining the API endpoints

Based on the documentation of the various stock market information providers I created one function per provider/URL combination that contains the essence of each API call, such as the URL and the required parameters. These functions take the JSON object/error message from the generic getAPIresponse() function above and apply domain specific logic to it. In the case the latest stock price was returned, the price is then stored and the promise is resolved without returning any value. On the other hand, if the API returns an error message, the message will be returned to the calling function to action on it.

// api.js

const IEX_SECRET_KEY = "1234";
const FINNHUB_SECRET_KEY = "abcd";

// defines the primary API endpoint to retrieve a stock's price
export async function getIEXquote(asset) {
  const url =
    "https://cloud.iexapis.com/stable/stock/" + asset.ticker + "/quote";
  let params = {
    token: IEX_SECRET_KEY
  };
  return new Promise(async function(resolve, reject) {
    handleApiResponse(url, params)
      .then(res => {
        asset.lastPrice = res.latestPrice;
        resolve();
      })
      .catch(err => reject(err));
  });
}


// defines an alternative API endpoint to retrieve a stock's price
export async function getFINNHUBquote(asset) {
  const url = new URL("https://finnhub.io/api/v1/quote");
  let params = {
    symbol: asset.ticker,
    token: FINNHUB_SECRET_KEY,
    adjusted: true
  };
  return new Promise(async function(resolve, reject) {
    handleApiResponse(url, params)
      .then(res => {
        asset.lastPrice = res.c;
        resolve();
      })
      .catch(err => reject(err));
  });
}

Putting it all together

The final step is to define in which order the API endpoints should be called. In the below example the second API call is only executed if the first one returns a rejected promise. If the second API call returns an error as well, e.g. in case no price could be retrieved for the given stock symbol, the error message will be stored in the custom asset object, so it can be retrieved later on.

// api.js

export async function getQuote(asset) {
  await getIEXquote(asset)
    .then(() => (asset.error = null))
    .catch(
      async err =>
        // in case of error try alternative API
        await getFINNHUBquote(asset)
          .then(() => (asset.error = null))
          .catch(err => (asset.error = err))
    );
}

Now all that is left to do to retrieve the latest price for a stock is to call the getQuote() function. This can be done from anywhere in the application after importing the api.js file first.

// main.js

import * as API from "./api";
// ...
API.getQuote(asset)

I hope this article gave you some ideas how to tackle similar issues you might have.

Top comments (0)