Using JavaScript generators to optimize APIs

Using JavaScript generators to optimize APIs

This post is inspired by this wonderful post by Evelyn Stender. This post does not explain what a generator is or basic usage, Evelyn's post does a good job of that. Instead, in this post, we will demonstrate how generators can be useful for streaming either a very large dataset or dataset requiring significant computation.

Say, we have a function, that returns a generator for getting accounts:

function* getAccounts(totalCount = 0) {
  while (totalCount > 0) {
    --totalCount;
    // assume complex computation for account balance.
    yield { userId: totalCount + 1, balance: Math.random() * 100 };
  }
}

Notice the use of yield, that way the function doesn't wait till all user accounts are processed before returning results. Next step, we need to get these accounts and do something with them; in this case, we will be returning them to a client.

app.get('/accounts', (req, res) => {
  const accountsGenerator = getAccounts(req.query.count);

  res.setHeader('Content-Type', 'text/html; charset=utf-8');
  res.setHeader('Transfer-Encoding', 'chunked');

  writeData(res, accountsGenerator);
});

function writeData(response, generator) {
  var account = generator.next();
  if (true === account.done) {
    response.end();
  } else {
    response.write(JSON.stringify(account.value));
    setTimeout(function () {
      writeData(response, generator);
    }, 0);
  }
}

This is an express route that streams accounts gotten from the generator (using a helper function). Putting it all together,

const express = require('express');
const bodyParser = require('body-parser');

const app = express();
const port = 3000;

app.use(bodyParser.json());

function* getAccounts(totalCount = 0) {
  while (totalCount > 0) {
    --totalCount;
    yield { userId: totalCount + 1, balance: Math.random() * 100 };
  }
}

app.get('/get-accounts', (req, res) => {
  const accountsGenerator = getAccounts(req.query.count);

  res.setHeader('Content-Type', 'text/html; charset=utf-8');
  res.setHeader('Transfer-Encoding', 'chunked');

  writeData(res, accountsGenerator);
});

function writeData(response, generator) {
  var account = generator.next();
  if (true === account.done) {
    response.end();
  } else {
    response.write(JSON.stringify(account.value));
    setTimeout(function () {
      writeData(response, generator);
    }, 0);
  }
}

app.listen(port, () => console.log(`App listening on port ${port}!`));

There are several improvements we can make to this solution (using correct headers, removing the recursive function, making the streaming of response better etc), but for simplicity's sake this works. For advantages of using this method instead of loading all accounts into memory before streaming see linked post.

Did you find this article valuable?

Support Iroegbu Iroegbu by becoming a sponsor. Any amount is appreciated!