Node, MVC Controllers and ECMAScript 2015

Long time readers of my blog will remember that I previously covered Node and MVC applications in a short tutorial series. At that point, I wrote the controllers and loader in ECMAScript 5.1. The code to load the controllers was this:

  fs.readdirSync("./controllers").forEach(function (file) {
    if (file.substr(-3) === ".js") {
      console.info("Loading Controller " + file);
      var base = "/" + path.basename(file, ".js");
      var route = require("./controllers/" + file);
      app.use(base, route);
    }
  });

The typical controller looked like this:

"use strict";

var express = require("express"),
    path = require("path"),
    config = require("../config.json"),
    extend = require("extend");

var router = express.Router(), // eslint-disable-line new-cap
    controller = path.basename(__filename, ".js"),
    loginRoute = config.loginRoute || "/account/login";

/**
 * Set of default properties for the rendering engine
 */
function defaultProperties(req) {
  return {
    title: "Unknown",   // Default title in case the developer doesn't set one
    user: req.user
  };
}

/**
 * Render an appropriate view
 */
function view(req, res, viewName, locals) {
  res.render(controller + "/" + viewName + ".html",
    extend({}, defaultProperties(req), locals));
}

/**
 * GET /{controller=Home}/index
 */
function index(req, res) {
  if (!req.isAuthenticated()) {
    res.redirect(loginRoute);
    return;
  }
  view(req, res, "index", { title: "Home" });
}

// Per-route functionality
router.get("/index", index);

// Default route is to GET index
router.get("/", index);

module.exports = router;

That’s a lot of boilerplate code and it’s completely unreadable for mere mortals. I am working on learning ECMAScript 2015 in depth and I thought I would re-visit this topic. Could I make my controller modules into classes and simplify the whole controller configuration?

Step 1: The New Controller Loader

Since my controllers are going to be classes, I knew I was going to need to adjust the loader. My aim here is just to load the controller classes; not to get the perfect class loader. Here is my code:

// MVC Controllers
var controllerList = {};
fs.readdirSync(path.join(__dirname, "controllers")).forEach(function (file) {
	if (file.substr(-3) === ".js") {
		var basePath = path.basename(file, ".js");
		var Controller = require(`./controllers/${file}`);
		controllerList[basePath] = new Controller(basePath);
		app.use(`/${basePath}`, controllerList[basePath].router);
	}
});

This code loops through every single JavaScript file in the controllers directory. For each one, it constructs a basePath. If the controller is called home.js, then the basePath becomes “home”. I then load the javascript file using a standard “require” statement. Since it’s a class, I create a new object passing in the basePath. I expect that object to expose a router parameter and I use that to link in the router to the basePath. I construct the path using an ES6 template string.

Step 2: The Controller – First Pass

One of the concepts I am pretty religious about is this: Don’t over-complicate the solution early. Get it working, then see if you can do something to make the solution simpler, easy to test, read better, more efficient or whatever you want to do. Here is my first controller:

var express = require("express"),
	extend = require("extend");

export default class HomeController {
	constructor(basePath) {
		this.basePath = basePath;
		this.router = express.Router(); //eslint-disable-line new-cap

		// Route definitions for this controller
		this.router.get("/", (req, res) => { this.index(req, res); });
		this.router.get("/index", (req, res) => { this.index(req, res); });
	}

	renderView(response, viewName, localData = {}) {
		let viewPath = this.basePath + "/" + viewName;
		let defaultData = {
			title: "~~~Unknown~~~"
		};
		let data = extend(defaultData, localData);
		response.render(viewPath, data);
	}

	// GET /home/index (or just /home)
	index(request, response) {
		this.renderView(response, "index");
	}
}

One of the things you will note is that this actually has a lot in common with the ECMAScript5 version of the same controller – it’s just in class form. My module exports the controller class by default. That means I can import it with whatever name I want. The constructor stores the base path that it is passed and creates a new router. The constructor also constructs the routes available in the controller. It uses ES6 fat arrows to preserve the “this” variable.

The renderView method combines a dataset with a view and renders a view. Note I’m using ES6 default parameters and the block-level “let” statement – more ES6 features in use.

Finally, I have a method that actually handles a route. Really, the only unique things for this controller are the index() method that handles a route and the route definition in the constructor.

Step 3: Simplify the Code

This controller class actually works, but I can see a bunch of duplicated code. In the constructor, the basePath and router will always be done this way. I’m allowing the underlying system access to the router variable – definitely not what I want. I want the router to be read-only after creation. Also, the renderView() method is going to be boiler plate code – I want to abstract that away. My simplifying thought here is this: let’s create a Controller class with all the common code in it. We can then extend the Controller class to create the HomeController and only include the “differences”. Here is my new Controller.js file:

var express = require("express"),
	extend = require("extend");

export class Controller {
	constructor(basePath) {
		this.prvBase = basePath;
		this.prvRouter = express.Router(); //eslint-disable-line new-cap
	}

	renderView(response, viewName, localData = {}) {
		let viewPath = this.prvBase + "/" + viewName;
		let defaultData = {
			title: "~~~Unknown~~~"
		};
		let data = extend(defaultData, localData);
		response.render(viewPath, data);
	}

	get basePath() {
		return this.prvBase;
	}

	get router() {
		return this.prvRouter;
	}
}

A lot of this code comes from the original home.js code. I’ve converted the basePath and router variables into ES6 getters so that I can make them read-only (assuming one uses the published API). Now, what does an implementation of a controller look like? Let’s look at the new home.js:

import {Controller} from "../mvc/Controller";

export default class HomeController extends Controller {
	constructor(basePath) {
		super(basePath);

		// Route definitions for this controller
		this.router.get("/", (req, res) => { this.index(req, res); });
		this.router.get("/index", (req, res) => { this.index(req, res); });
	}

	index(request, response) {
		this.renderView(response, "index");
	}
}

This is getting much closer to where I want to be. The super() call is for running the constructor of the class I am extending, thus setting up the common code. I still have to define the routes within the constructor, but I don’t have any boiler-plate code.

What else would make this controller object awesome? ECMAScript 7 is defining a concept called decorators. With ES7 decorators, I could theoretically do something like this:

export default class HomeController extends Controller {
    @HttpGet([ "/", "/index" ])
    index(request, response) {
        this.renderView(response, "index");
    }
}

Of course, ES2015 (also known as ES6) is just out and I don’t expect ES7 to be ratified for a number of years, so there is no guarantee that anyone will support ES7 decorators. You can check out the compatibility chart at Kangax. Note that only Babel supports ES7 decorators right now.

I’m going to delve into decorators in another blog post. For now, this mechanism is great for developing an MVC application without the need of libraries beyond the standard ExpressJS.