Convention-based, Modular MVC with Shared Rendering in Node.js

by Nicolas Bevacqua on June 11, 2014

The modern web is always changing, and this article is more than two years old.

By Nicolas Bevacqua

Taunus is a new framework for Node.js that aims to simplify the state of MVC and shared rendering. You may already be rolling your eyes saying, “Another framework?” However, in this article I hope to explain some of the motivation for creating Taunus (its inspiration and my initial goals), what makes it different, and show you how to get started. While it is stil early in development, I think there is potential worth exploring.

Background – Why I Created Taunus

I believe Taunus is interesting, not because it introduces innovative paradigm shifts or the like, but rather, because it takes a proven concept, and iterates upon it. Taunus builds upon the design of Rendr. By all accounts, Rendr was amazing. I read a lot about Rendr before trying it out. I was really excited about it. What could be wrong? I mean, you had convention over configuration, shared rendering, and reused modules. If you’ve tried either Ruby on Rails or ASP.NET MVC 3+, both of those follow a similar convention-based architecture. Eventually, I had to use Rendr in order to determine if it was “good enough” to recommend it in my JavaScript Application Design book as the preferred approach for shared-rendering in large scale applications.

Assuming it was as good as advertised, I could just write about Rendr in my book and forget about shared rendering. I’d figured it’d be mostly a drop-in plugin for Backbone. I was thorougly wrong.

Getting started with the bare minimum viable Rendr is nothing short of painful. You can check it out for yourself. The so-called “simple” example in Rendr’s repository on GitHub almost left me in tears.

I get it. Rendr requires Backbone. Backbone requires jQuery. Fine, I accept those terms. Rendr takes a “Convention over Configuration” approach. That’s awesome! I loved that back when I was involved in C# MVC application development. Except Rendr isn’t so much about “Convention over Configuration”, but more about “Convention, deal with it,” to the point where you must conform to multiple different rules.

  • You must browserify your client-side code in obscure ways:
    • brfs won’t work at all.
    • jQuery must be shimmed even though it’s available on npm.
    • Furthermore, the server-side piece of Rendr uses a hard-coded version of jQuery, independently of the one you pick for the client-side.
    • You must define aliases in a so-and-so way (Richard Feynmann tainted my writing style, so be it).
  • Your templates must be placed into app/templates/compiledTemplates.js. Seriously?
  • Handlebars. Backbone. jQuery. Deal with it.
  • Awkward APIs like this.app.fetch
  • Thorough lack of documentation.

After asking Spike Brehm about it on Twitter, and figuring out he’s pretty much moved on from Rendr, I decided not to adopt it and strike out on my own.

This is how I ended up creating Taunus.

Putting together a client-side MVC framework is no easy feat. I should know. I tried it back in the day, when putting together my blog engine. I failed misreably back then.

This time, though, I made an effort to mix the best of both worlds. I decided to go back to basics and write a framework that was built around modularity, MVC, events, and shared-rendering.

Taunus Architecture Overview

The hardest dependency you’ll find in Taunus is on Express. Taunus itself doesn’t utilize Express, but it expects routes to be in the same format that Express expects, and it also expects you to pass it an object with a get method, which will be called once for every route, with a few middleware functions. As long as you’re able to comply with those terms, Taunus will work well for you.

The routing constraint is particularly interesting, because Taunus uses the Express router on the client-side. Thus, using that same router in the server-side becomes a necessity for consistency. That being said, there’s nothing to stop you from making use of routes on an http server instance, pass a { get: fn } object to taunus.mount, and ditch Express!

Taunus deals mainly in the four components explained below. When Taunus is hit with a request, the router will call your controller action on the server-side. Once the controller action method is done, the view renderer will take the model provided by the action and render the view. The server is now done.

When the client-side starts up, it’ll invoke the client-side controller for that view. Suppose now that another page is requested. Taunus will query that same endpoint using an Accepts: 'application/json' header, and the server will return JSON instead of the fully rendered page. Taunus uses that JSON data on the client-side to render your view, and then calls the client-side controller, just like it did on page load.

taunus_architecture

It’s easiest to explain how Taunus works with an example walk-through.

  1. Incoming request /articles/taunus-is-awesome
  2. Router matches /articles/:slug, and invokes the controller for that action
  3. The articles/slug action controller fetches a model from the database
  4. In the controller action, we assign a model to res.viewModel, and call next()
  5. Taunus renders the partial for this view, and wraps it with the view layout
  6. The client-side takes over, and it figures out that it’s the first time around, so it doesn’t re-render the view
  7. The client-side view controller gets invoked.

That’s as far as first-time execution goes. Let’s move on.

  1. You click on “About this blog”, which is a link to /about
  2. Taunus captures that click and issues an XHR request for /about, asking for JSON data
  3. Incoming request /about
  4. Router matches /about, and invokes the controller for that action
  5. The home/about action controller fetches a model from the database
  6. In the controller action, we assign a model to res.viewModel, and call next()
  7. Taunus responds with the JSON model as-is
  8. The client-side takes over, this time it renders the view, using the model from the AJAX call
  9. The client-side view controller gets invoked

Note how steps 3 through 6 are exactly the same in both cases. Note how the last step is the same, too. Also note that the steps that are different are dealt with by Taunus, so you don’t have to worry about them.

The cool part about Taunus is that it doesn’t rely on Backbone, or jQuery, or any client-side libraries. That’s up to you. Taunus only handles view routing and the purely MVC aspect of your application. Since it’s Common.JS, you get dependency injection for free, and the convention-based approach means that you won’t have to worry about routing in more than one place. You can use the browser’s native APIs for pretty much everything you want, meaning Taunus doesn’t want you to marry it. That’s a pretty good thing, because it’s a car model line, and that’d be awkward.

It doesn’t really break from what you’re used to doing in Express. You just do your thing, set a view model on the response, and call next(). Let’s see how Taunus is actually used.

Taunus On The Server

Taunus provides three different components that operate together, offering a sane development model that doesn’t get in your way. Let’s start with the routes. In a typical Express application, you’d register routes by hand, like below.

app.get('/pony/foo', pony.foo);
app.get('/pony/bar', pony.bar);
app.get('/author/moo', author.authenticated, author.moo);

With Taunus, you’re expected to define your view routes in a JSON format instead. It’s recommended that you put these into an individual module, like below. Note that, under the default module path resolver, controllers are expected to be defined on a per-action basis, and the same goes for views. In fact, the action property in the route is used to infer which view should get rendered by Taunus.

var authorAuthenticated = require('./author/authenticated');

module.exports = [
  { route: '/pony/foo', action: 'pony/foo' },
  { route: '/pony/bar', action: 'pony/bar' },
  { route: '/author/moo', action: 'author/moo', middleware: authorAuthenticated }
]

Once you’ve created the routes module, you can boot up using the command taunus. Here’s a module which takes an Express instance and creates routes in it. Note how I’m perfectly able to mix my Taunus view routes with any other routes I have, such as API routes or error handling middleware.

var taunus = require('taunus');
var routes = require('./routes');
var articleList = require('./article/list');
var errors = require('../lib/errors');

module.exports = function (app) {
  app.get('/api/articles', articleList);
  taunus.mount(app, routes);
  app.use(errors.handler);
};

A controller action might look like the snippet below. The model property on the view model is what gets passed to partial view template functions. The layout will get the full viewModel instead.

module.exports = function (req, res, next) {

  // fetch data from somewhere
  // var user = ...

  // assign it to the view model
  res.viewModel: {
    model: {
      title: 'User Profile',
      user: user
    }
  };

  // we're done, yield control over to the renderer
  next();
}

Sharing Routes and View Templates

The client-side portion of your application is where things get a bit more interesting. Like I explained back in the architecture section, the client-side will use the same routes and view templates that the server-side uses. Here’s where we run into trouble. Browserify is awesome; seriously, ragingly awesome. It does come with certain limitations, such as the inability to parse dynamically composed require(expr) expressions at compile time. Browserify is only smart enough to figure out how to unwrap require('expr') calls. The issue is that you want to initialize Taunus on the client-side as well, and for that you’ll need a routes object which looks somewhat like the snippet of code below.

module.exports = [{
  route: '/',
  template: require('../views/home/index'),
  controller: require('../../client/js/controllers/home/index')
}, {
  route: '/author/compose',
  template: require('../views/author/compose'),
  controller: require('../../client/js/controllers/author/compose')
}];

The above might look fine for two routes, but imagine maintaining that by hand? It’s pointless! You already have a routes module – the one you used on the server. Surely you can build a small script that turns your server-side routes into these client-side routes in a heartbeat? That’s what I did!

Taunus On The Command-Line

Taunus comes with a small CLI interface that can compile your routes. Without any options, the program will print a client-side routing module to standard out.

taunus

The CLI comes with a few options. Currently, these options are available, among others that aren’t worth mentioning in this article.

Option Description
-o Instead of stdout, the output is dumped to the client_routes file, as defined in .taunusrc
-w Watch for changes to the server-side routes and recalculate the output. Works well with -o
--standalone path/to/file Export taunus as a global, and your routes, using a single stand-alone bundle file

Taunus wants you to use Browserify really badly, although you still have the option of not using CommonJS in your own code – and that’s okay.

Can I avoid using Browserify?

Yes! However, you’d still have to compile Taunus using Browserify, like below. This command won’t just Browserify Taunus, but it’ll also compile your route definitions into client-side routes, and place them in taunus.routes. The -w flag still works just fine. -o is implied.

taunus --standalone client/js/vendor/taunus.js -w

Once you’ve compiled your routes and browserified your bundle, you can set up Taunus on the client-side.

Taunus on the Browser

If you went for Browserify, the code will be nicely modular. Precious, _precious modularity. Pat yourself on the back. Click here for an example build file.

var taunus = require('taunus');
var routes = require('path/to/client-side/routes');
var elem = document.querySelector('main');
taunus.mount(elem, routes);

If you’ve decided to ditch Browserify, then it’ll be even easier to set up.

var elem = document.querySelector('main');
taunus.mount(elem, taunus.routes);

Once that’s set up, all you’ll need to do is create your routes, view templates, server-side and client-side controllers! Taunus will be mostly be out of your way. You can, however, get in the way of Taunus, by listening to the events it emits and reacting to them.

Conclusion

There’s still a long way to go, but I plan to continue to improve on the framework as I toy with it and learn about its weaknesses. What do you think about Taunus?

This article was originally published at http://blog.ponyfoo.com/2014/05/21/taunus-micro-isomorphic-mvc-framework