Making APIs Reusable with Promises

Written by Tom Wilkins
Mon, 30 Sep 2019

Use this technique to make external APIs more resuable

Making APIs Reusable with Promises

External APIs are fantastic for enhancing your application. I've used them frequently to fetch data, and to trigger actions on an external service, for example Google Drive.

As I've used these more often, I've come to realise that they aren't particularly reusable in their basic form. However, with promises, we can convert these into reusable modules that can be used anywhere in our application.

This article assumes some knowledge of Node.js, as I'm coming at it from a command line application angle. I've used this approach for a lot of the scheduled tasks required at work.

I'll be using the Google Sheets API as an example, which is brilliant for getting datasets into your application quickly. You can find documentation on that, and other Google APIs on GitHub.

What do you mean, "they aren't reusable"?

OK, so a lot of these external APIs could be reusable without promises, but I think promises make it a lot easier and more readable.

Let's start with the basic code for reading a Google Sheet:

const { google } = require('googleapis');
const scopes = ['https://www.googleapis.com/auth/spreadsheets'];
const jwt = require('./path-to-auth.json');
const auth = new google.auth.JWT(
jwt.client_email,
null,
jwt.private_key,
scopes
);
const sheets = google.sheets({
version: 'v4',
auth,
});
sheets.spreadsheets.values.get(
{
auth: auth,
spreadsheetId: 'SpreadSheetID',
range: 'Data!A:E',
},
function(error, res){
if(error) throw error;
const rows = res.data.values;
// do something with the data
}

As you can see, the sheets.spreadsheets.values.get method requires a callback function. It's inside this callback that you need to work with the data that comes back. This is a common pattern you will find getting data from external sources, and once upon a time it was the only way to do so.

We could make this code a bit more resuable by wrapping the code in a function that takes our callback as an argument, like so:

function readGoogleSheet(spreadsheetId, range, callback) {
// skipping some lines
sheets.spreadsheets.values.get(
{
auth,
spreadsheetId,
range
},
function(error, res) {
if (error) throw error;
const rows = res.data.values;
callback(rows);
}
);
}

We are now using the spreadsheet ID, the data range, and a callback function as parameters which means this function can be reused anywhere in our application. This improves things a lot, but it could be better still. Let's say that we have more than one spreadsheet to grab data from, or another service altogether, are we going to nest these callbacks? That could become messy very quickly.

Using Promises

Promises help us get around this. Anything that returns a promise will either resolve, or reject. They are used in situations where we know the result won't be available immediately, and the beauty of them is we can chain a .then() to them, or use them with async/await.

Taking our original spreadsheet script, let's rework it to make use of promises.

function readGoogleSheet(spreadsheetId, range) {
const { google } = require("googleapis");
const scopes = ["https://www.googleapis.com/auth/spreadsheets"];
const jwt = require("./path-to-auth.json");
const auth = new google.auth.JWT(
jwt.client_email,
null,
jwt.private_key,
scopes
);
const sheets = google.sheets({
version: "v4",
auth
});
return new Promise(function(resolve, reject) {
sheets.spreadsheets.values.get(
{
auth,
spreadsheetId,
range
},
function(error, res) {
if (error) reject();
const rows = res.data.values;
resolve(rows);
}
);
});
}

When creating a new promise, you need to provide it with a function. This function takes two parameters, resolve and reject, which are both functions. If something goes wrong when requesting the data, you can invoke the reject function. Otherwise, you can invoke the resolve function with the data. In the case above, we are resolving our promise with the rows from our Google Sheet.

Now What?

The readGoogleSheet function is now reusable, but how do we actually use it? We have two options.

.then()

The .then() method can be chained to anything returning a promise. It's inside this method that we can work with our data, like so:

readGoogleSheet("exampleSpreadsheetId", "exampleRange").then(data => {
// do something with the data
});

This is a bit cleaner but we are still faced with chaining when more APIs come into play. To work with our data, we must be inside this then method. So what is the other option?

async/await

The await keyword allows us to work with promises in a completely different way. This keyword will pause script execution until the promise is resolved, which makes for a far cleaner syntax that makes it far easier to work with numerous APIs.

The one catch is that the await keyword must appear within an asynchronous function. Let's see how this looks:

async function(){
const data = await readGoogleSheet('exampleSpreadsheetId', 'exampleRange');
}

Here the function is declared as asynchronous, and to work with our data we simply assign it to a new variable using await.

Here's a use case I ran into recently where using async await was the clear option to go with:

  1. Grab some data from one API
  2. Use the result from that API to query a second API
  3. Use the result from the second API to get data from a third API
  4. Write to a spreadsheet using data from all three APIs

Now, three levels is not that deep, but it still would have been an unreadable mess had I not used async/await. Larger applications can require far more external APIs than this, so you can imaging how convoluted that can get.

Final thoughts

I am still very new to promises but this has been a real lightbulb moment for me so I felt compelled to write a blog post. It's likely I'll either revisit this one and/or write further posts on this in future as I'm sure there's a lot left to learn.

Thank you for reading

You have reached the end of this blog post. If you have found it useful, feel free to share it on Twitter using the button below.

Tags: TIL, Node.js, JavaScript, Blog, Promises, Functional Programming