arni
arni

Reputation: 78

Combine results from multiple Node.js API calls

New to Node.js here. I'm looking for the correct way to make N asynchronous API calls from within another function, and combining their results to use further downstream. In my case, N would be reasonably small and blocking for their execution not too bad.

In synchronous execution the implementation for combine() below should work.

If I only needed the results from one API call it would be straightforward to implement the following logic in a callback function supplied to callAPI(). Where I stumble is when I need all the results combined before before executing foo(total, [...args]).

I looked into async.whilst but wasn't able to get that to work. I'm skeptical if that actually is the correct fit to my needs. I've also looked into Promises which seems to be the correct lead but it would be nice to get reassurances before crawling into that cavernous rabbit hole. Be it that Promises is the correct way, which module is the standard to use in Node.js projects?

var http = require('http');

function callAPI(id) {
    var options = {
        host: 'example.com',
        path: '/q/result/'.concat(id)
    }

    var req = http.get(options, (res) => {
        var body = [];
        res.on('data', (chunk) => {
            body.push(chunk);
        }).on('end', () => {
            body = Buffer.concat(body).toString();
            return body;
        }).on('error', (err) => {
            console.error(err);
        });
    });
}

function combine(inputs) {

    var total = 0;
    for (i=0; i < inputs.length; i++) {
        total += callAPI(inputs[i]['id']);
    };
    console.log(total);

    // call some function, foo(total, [...args])
}

Edit 1:

I attempted to follow samanime's answer below and modify the API call to return a Promise. See:

function callAPI(id) {
    return Promise((resolve, reject) => {
        var options = {
            host: 'example.com',
            path: '/q/result/'.concat(id)
        }

        var req = http.get(options, (res) => {
            var body = [];
            res.on('data', (chunk) => {
                body.push(chunk);
            }).on('end', () => {
                body = Buffer.concat(body).toString();
                resolve(body);
            }).on('error', (err) => {
                reject(err);
            });
        });
    });
}

function combine(inputs) {

    var combined = [];
    for (i=0; i < inputs.length; i++) {
        total += callAPI(inputs[i]['id']);
            .then(result => {
                combined.push(result);
            });
    };
    var total = combined.reduce((a, b) => a + b, 0);
    console.log(total);

    // call some function, foo(total, [...args])
}

This seems to get me halfway there. If I console.log(combined) inside the then() block I can see the list building up with results from the API calls. However, I still can't access the complete combined at the "end" of the for loop. Can I attach a callback to something to run after the full list has been built? Is there a better way?

Edit 2 (My solution - per Patrick Roberts suggestion)

function callAPI(id) {
    return Promise((resolve, reject) => {
        var options = {
            host: 'example.com',
            path: '/q/result/'.concat(id)
        }

        var req = http.get(options, (res) => {
            var body = [];
            res.on('data', (chunk) => {
                body.push(chunk);
            }).on('end', () => {
                body = parseInt(Buffer.concat(body));
                resolve(body);
            }).on('error', (err) => {
                reject(err);
            });
        });
    });
}

function combine(inputs) {
    var combined = [];
    Promise.all(inputs.map(input => callAPI(input.id)))
        .then((combined) => {
            var total = combined.reduce((a, b) => a + b, 0);
            // foo(total, [...args])
        });
};

Upvotes: 2

Views: 5150

Answers (3)

Patrick Roberts
Patrick Roberts

Reputation: 51866

Your edit is looking a lot better, but try this:

function callAPI(id) {
  return Promise((resolve, reject) => {
    var options = {
      host: 'example.com',
      path: '/q/result/' + id
    }

    http.get(options, (res) => {
      var body = [];
      res.on('data', (chunk) => {
        body.push(chunk);
      }).on('end', () => {
        body = Buffer.concat(body).toString();
        resolve(body);
      }).on('error', reject);
    });
  });
}

function combine(inputs) {
  Promise.all(inputs.map(input => callAPI(input.id))).then((combined) => {
    // completed array of bodies
    console.log(combined);
    // foo(combined.length, [...args]);
  }).catch((error) => {
    console.log(error);
  });
}

Upvotes: 0

Peheje
Peheje

Reputation: 14194

I would add a counter that keeps track of remaining API calls. Whenever an API call finishes, decrement and if its 0, you're done.

const numCalls = 10;
let remaining = numCalls;
let data = [];

function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min)) + min;
}

function ajax() {
    // Simulate ajax with a setTimeout for random amount of time.
    setTimeout(() => {
        // This is the callback when calling http.get
        data.push(getRandomInt(0, 10)); // Some data from server
        if (--remaining <= 0) {
            // Am I the last call? Use data.
            console.log(data);
            console.log(data.length);
        }
    }, getRandomInt(1000, 3000));
}

for (let i = 0; i < numCalls; i++) {
    ajax();
}

Upvotes: -1

samanime
samanime

Reputation: 26537

It sounds like you can just chain together a bunch of promises, passing the data along.

Basically something like:

const combined = [];
asyncOne()
  .then(result => { combined.push(result); return asyncTwo())
  .then(result => { combined.push(result); return asyncThree())
  // and so on

As long as each function returns a promise, you'll be all set.

If you want to run them in parallel, use Promise.all(), which will do the same thing for you:

Promise.all([asyncOne(), asyncTwo(), asyncThree() /* , etc */])
  .then(combined => /* combined is an array with the results of each */)

This is by far the preferred pattern for this sort of thing.

Upvotes: 3

Related Questions