Web apps: validate, delegate, respond

Web apps should be a thin, easily replacable front to your business logic. It should be modular and composable. I have written about this before. But how do you — in practice — decouple the business logic from the web frontend?

I like to use a simple rule, which states that a web app has three (and only three) responsibilities: It must validate, delegate and respond, and nothing else.

It validates the input gotten from users through HTTP requests, and ensures that it lives up to the requirements presented by the business logic.

Then it delegates, passing the arguments on to the business logic. The web app basically lets go of the input at this point, and never touches it again. There might be some slight transformation needed, but if your business logic has an appropriate interface this should be minimal.

Finally, when the business logic returns the web app responds, either succesfully or with an error. This means presenting a meaningful message to the user.

That’s the basic principle. The rest of this post will be an example, to show the principle in action, and the separation it enforces.

Example: express #

Let’s write an endpoint that does everything, and then move towards removing everything that falls outside of the three categories of responsibility defined above.

This example will use a Node.js express app. Endpoints in express are defined as functions that take a request object req and a response object res.

This endpoint additionally depends on a URL for the CouchDB database it saves data in. It responds to PUT /:id requests and requires a request body containing a name. A correct request will result in updating a user’s name.

// in case you're wondering, this is the "bad" example
function modifyUser(couchUrl, req, res) {
  var db = require("nano")(couchUrl).use("users");
  var id = req.params.id;
  var name = req.body.name;

  if(!name) {
    return res.status(400).send("Missing 'name'");
  }

  db.get(id, function(error, userDoc) {
    if(error && error.statusCode == 404) {
      return res.status(404).send("User " + id + " not found.");
    }
    if(error) {
      return res.status(500).send("Failed to get user " + id);
    }

    userDoc.name = name;

    db.insert(userDoc, function(error) {
      if(error) {
        return res.status(500).send("Failed to update user");
      }

      res.send("Updated user's name");
    });
  });
}

// a creation function that returns an express endpoint:
module.exports = function(couchUrl) {
  return modifyUser.bind(this, couchUrl);
}

The endpoint starts off with instantiating the database that is used to make requests. That definitely doesn’t fall within any of the three categories. We can solve this by making the endpoint depend on an already-instantiated database instead:

// this is still bad, though
function modifyUser(db, req, res) {
  // ...
}

// we now depend on a db
module.exports = function(db) {
  return modifyUser.bind(this, db);
}

The next few lines in modifyUser get the required variables from the URL and the request body. We know that id is set, because that’s the only way this endpoint would be matched, and the endpoint verifies that a name has been passed in through the body of the request. All of this falls within the responsibility of validation.

Next up comes the bulk of the code: a lot of database interaction. We can extract all of this to a separate function:

// getting somewhere, but still not great
function modifyUser(db, req, res) {
  var db = require("nano")(couchUrl).use("users");
  var id = req.params.id;
  var name = req.body.name;

  if(!name) {
    return res.status(400).send("Missing 'name'");
  }

  updateUserName(db, id, name, function(error) {
    if(error && error.statusCode == 404) {
      return res.status(404).send("User " + id + " not found.");
    }
    if(error) {
      return res.status(500).send("Failed to update user");
    }
    res.send("Updated user's name");
  });
}

function updateUserName(db, id, name, callback) {
  db.get(id, function(error, userDoc) {
    if(error) {
      return callback(error);
    }

    userDoc.name = name;

    db.insert(userDoc, callback);
  });
}

module.exports = function(db) {
  return modifyUser.bind(this, db);
}

This extraction leaves us with some code that looks a bit silly. We now depend on db simply to pass it on to a function.

We have the overall structure down in the modifyUser function: we validate, then delegate to updateUserName, and finally respond to the user depending on what updateUserName tells us.

The alternative is to simply depend on an outside updateUserName function, instead of a specific db. Making updateUserName an external dependency means that all of the business logic is injected; we only validate, delegate and respond.

The business logic can now change and be optimized without any involvement of the endpoint.

The final code looks like this:

// pretty good, pretty simple
function modifyUser(updateUserName, req, res) {
  var id = req.params.id;
  var name = req.body.name;

  if(!name) {
    return res.status(400).send("Missing 'name'");
  }

  updateUserName(id, name, function(error) {
    if(error && error.statusCode == 404) {
      return res.status(404).send("User " + id + " not found.");
    }
    if(error) {
      return res.status(500).send("Failed to update user");
    }
    res.send("Updated user's name");
  });
}

module.exports = function(updateUserName) {
  return modifyUser.bind(this, updateUserName);
}

Still hungry for more? Sign up for my newsletter.

 
7
Kudos
 
7
Kudos

Now read this

I wouldn’t recommend Scrum

I wouldn’t recommend Scrum. Scrum is a software development process, and in order to “use Scrum” you have to adopt all of it. The process has a lot of good ideas, like daily standup meetings. It is possible to practice Extreme... Continue →