How to stream results of multiple Promises in Node.js

Recently I came across a task to create a small service which will query several external APIs and output their statuses. Each of the call to the API takes from half a second to up to couple of seconds. Eventually those statuses are meant to be returned as an API response.

We have 4 modules that are SDKs for each external API, for the sake of simplicity lets pretend those SDKs have the same name of the method that we are going to call to fetch the status. First we start with the code below:

router.get('/api/status', async(req, res) => {
    let api1Status = await api1.getStatus();
    let api2Status = await api2.getStatus();
    let api3Status = await api3.getStatus();
    let api4Status = await api4.getStatus();

    return res.json({api1Status, api2Status, api3Status, api4Status})
})

That's the most naive way of doing this task. We synchronously call a service and wait for the response then call the next one. We are wasting time here. Using Node.js, parallelizing that work is no big deal. We can schedule all requests using Promises at the same time and then wait for all of them to finish using `await Promise.all([])`. The way to achievie that is illustrated by an example below:

router.get('/api/status', async(req, res) => {

    let status = Promise.all([
        api1.getStatus, api2.getStatus, api3.getStatus, api4.getStatus
    ]);

    return res.json(status)
})

The status will be returned only after all of the Promises resolve/reject their work. Please forgive me the ommition of `.catch()` block as its not our focus right now.

With this solution we are bound to the longest query execution. That's not ideal. What if we want to return partial response as soon as we receive the first result?

router.get('/api/status', async(req, res) => {

    let calls = [api1.getStatus, api2.getStatus, api3.getStatus, api4.getStatus]
    let callsMade = calls.length
    
    let cb = result => {
        res.write(JSON.stringify(result)+"\n")
        if(--callsMade == 0){
            res.end()
        }
    }

    calls.forEach(call => {
        call().then(cb)
    })
})

We end up with a little bit longer code, but its the fastest we can get! The idea here is to send each status in separate line as a JSON using `res.write()` method. The client will receive each line separately as soon as it's available on our end. Let me describe the parts of the code.

calls.forEach(call => {
    call().then(cb)
})

For each of the call we invoke it and since it returns a Promise we can use the native way to receive results from it. As a callback we pass `cb` function.

let cb = result => {
    res.write(JSON.stringify(result)+"\n")
    if(--callsMade == 0){
        res.end()
    }
}

Each time this fuction is called it does two things. First is to serialize the message and send it to client with `res.write()` method, after that it checks if that was the last call by decreasing the counter. If it is the last call then it closes the response with `res.end()`.

That's it, we streamed the results as soon as we get them and can provide the client with information he needs in the fastest way possible. Thanks for reading!



Similar searches: nodejs promises streaming

These posts might be interesting for you:

  1. Handling Promise rejections in Express.js (Node.js) with ease
  2. Klaster RabbitMQ na Dockerze z Nodejs w 5 minut
Author: Peter

I'm a backend programmer for over 10 years now, have hands on experience with Golang and Node.js as well as other technologies, DevOps and Architecture. I share my thoughts and knowledge on this blog.