Steph
Steph

Reputation: 27

Is there a way around nesting multiple functions as callbacks in Node.js/Express web apps to achieve sequential processing?

I am writing a simple URL shortening web app to learn JavaScript, Node (focusing on Express), and Mongo. A user submits a URL to my web app and receives a shortened URL (if the submitted URL is a valid URL) or an error message (if the submitted URL is not a valid URL). I have implemented the following algorithm:

  1. Check if the submitted URL is a valid URL (using dns.

The JavaScript code is below. I believe it works (based on some manual testing), but I have the following questions:

app.post("/api/shorturl", handleShortenURL);

function handleShortenURL(req, res) {
  console.log("Given URL: " + req.body.url);
  // Check if the provided URL is valid and return otherwise. dns.lookup accepts hostnames without the protocol prefix,
  // so if there is a protocol prefix in the URL, it needs to be removed  
  const REPLACE_REGEX = /^https?:\/\//i;
  let url = req.body.url.replace(REPLACE_REGEX, '');
  dns.lookup(url, function onLookup(err) {  
    if (err)
    {
      console.log("err.code = " + err.code);
      res.json({"error":"invalid URL"});
    }
    else  // The provided URL is a valid URL
    {
      // It the URL is already in the database, don't add it again
      URLModel.find({ original: url }, function (err, docs) {
        if (err)
          return console.log(err);
        
        if (Object.keys(docs).length > 0)
          res.json({original_url: req.body.url, short_url: docs[0].shortened});
        else
        {
          URLModel.find().exec(function (err, results) {
            let count = results.length;
            var urlDocument = new URLModel({original: url, shortened: count});
            urlDocument.save(function(err, data) {
              if (err)
                console.error(err);
            });

            res.json({original_url: req.body.url, short_url: count});
          }); 
        }
      });
    }
  });
  
}

This question is at a high-level similar to mine, but the specific approach for addressing it proposed by @OlivierKrull is somewhat different (it uses async/await along with Promises) and I find it easier to follow and, perhaps, a bit more elegant than the approach proposed at the above link.

Upvotes: 0

Views: 75

Answers (2)

Getter Jetter
Getter Jetter

Reputation: 2081

In order to omit nested callbacks like this you can use async/await.

mongoDB queries do return promises, so you can easily await the queries to resolve.

Not sure if dns.lookup returns a promise but if it does you could also just use await there.

I simplified your code and didn't include the error handling. But it should give you an idea of your possibilities.

app.post("/api/shorturl", handleShortenURL);

function handleShortenURL(req, res) {
  const REPLACE_REGEX = /^https?:\/\//i;
  const url = req.body.url.replace(REPLACE_REGEX, '');
  dns.lookup(url, async function onLookup(err) { 

    if (err) {
      console.log("err.code = " + err.code);
      return res.json({"error":"invalid URL"});
    }

    const docs = await URLModel.find({ original: url });

    if (Object.keys(docs).length > 0) {
      return res.json({original_url: req.body.url, short_url: docs[0].shortened});
    }

    const results = await URLModel.find();
    const count = results.length;
    const urlDocument = new URLModel({original: url, shortened: count});
    await urlDocument.save();
    return res.json({original_url: req.body.url, short_url: count});
  });
}

Upvotes: 1

tarkh
tarkh

Reputation: 2549

You can go async way and use Promise/await. Below is just an example, but you can adopt it for your program

// Use async
(async () => {
  // Function 1
  const fn1 = (val) => {
    return new Promise((resolve, reject) => {
      // Do some stuff here
      val = val + 1;
      // Resolve result.
      // Can be resolved from any level
      // of nested function!
      function nested1() {
        function nested2() {
          resolve(val);
        }
        nested2();
      }
      nested1();
    });
  };
  
  // Function 2
  const fn2 = (val) => {
    return new Promise((resolve, reject) => {
      // Do some stuff here
      val = val * 2;
      // Resolve result
      resolve(val);
    });
  };
  
  // Function 3
  const fn3 = (val) => {
    return new Promise((resolve, reject) => {
      // Do some stuff here
      val = val + 1000;
      // Resolve result
      resolve(val);
    });
  };
  
  // Sync code
  let val = 5;
  
  val = await fn1(val); // Wait until fn1 resolves
  val = await fn2(val); // Wait until fn2 resolves
  val = await fn3(val); // Wait until fn3 resolves
  
  console.log(val);
})();

Upvotes: 0

Related Questions