MVC Architecture – Node Style

In the last three articles I’ve been doing a study of what it takes to do a node app. For those who are starting here, I had a list of requirements:

  1. I need to be able to run a small web server
  2. I need to be able to handle templated views with server-side code
  3. I need to be able to do social authentication
  4. I need to be able to use an MVC architecture
  5. I need to be able to provide a Web API
  6. I need to be able to publish a node app to Azure
  7. I need to be able to edit node applications in Visual Studio 2015

I didn’t like the way I was writing code in the first three articles. It looked a little hacky. I wanted to get a clear separation of concerns – something the MVC pattern provides and something I am using in my ASP.NET vNext projects. I finally got there, but it was a lot of code.

Let’s start with the package.json – I needed a bunch more libraries to do MVC than I was using before. Here is my new package.json file:

{
  "name": "basic-webapp",
  "version": "0.0.1",
  "description": "A Basic View-Controller Web Application",
  "main": "server.js",
  "private": true,
  "scripts": {
    "start": "node server.js"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/adrianhall/node-stuff"
  },
  "author": "Adrian Hall ",
  "contributors": [
    {
      "name": "Adrian Hall",
      "email": "adrian@shellmonger.com"
    }
  ],
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/adrianhall/node-stuff/issues"
  },
  "homepage": "https://github.com/adrianhall/node-stuff",
  "dependencies": {
    "body-parser": "^1.12.3",
    "cookie-parser": "^1.3.4",
    "ejs": "^2.3.1",
    "express": "^4.12.3",
    "express-partials": "^0.3.0",
    "express-session": "^1.11.1",
    "extend": "^2.0.1",
    "method-override": "^2.3.2",
    "passport": "^0.2.1",
    "passport-auth0": "^0.2.1",
    "serve-favicon": "^2.2.0"
  }
}

Some of these are not strictly necessary. For example, I could happily dispense with the favicon. However, I wanted for the setup to be as complete as possible. Note that I’ve added a script as well – to start the server I now use npm start instead of node index.js. It also starts a different file – server.js.

In order to configure the application, I added a configuration JSON file called config.json. Note that the GitHub Repository has the file config-default.json – that’s because YOU MUST EDIT THIS FILE BEFORE USE. Here is the config-default.json file:

{
  "loginRoute": "/account/login",
  "server": {
    "uri": "http://localhost:3000",
    "port": 3000
  },
  "passport": {
    "auth0": {
      "domain": "{{DOMAIN}}.auth0.com",
      "clientid": "{{CLIENTID}}",
      "clientsecret": "{{CLIENTSECRET}}",
      "connections": [ "facebook", "windowslive", "google-oauth2", "twitter" ]
    }
  }
}

If you remember from the social authentication article, I’m using Auth0 as my social authenticator. That system requires a domain, client ID and client secret from the Auth0 website to function. Replace the appropriate pieces in this file and save it as config.json

To make sure that I don’t check in MY config.json, I’ve added it to the .gitignore file.

Talking of auth0, my auth0-strategy.js file is slightly different as well:

/*eslint-env node */

"use strict";

var passport = require("passport"),
    Auth0Strategy = require("passport-auth0"),
    config = require("./config.json");

var strategy = new Auth0Strategy({
  domain: config.passport.auth0.domain,
  clientID: config.passport.auth0.clientid,
  clientSecret: config.passport.auth0.clientsecret,
  callbackURL: "/account/external-callback"
}, function(accessToken, refreshToken, extraParams, profile, done) {
  return done(null, profile);
});

passport.use(strategy);

// This is not a best practice, but we want to keep things simple for now
passport.serializeUser(function(user, done) {
  done(null, user);
});

passport.deserializeUser(function(user, done) {
  done(null, user);
});

module.exports = strategy;

It reads the config.json file and inserts the information you stored there into the constructor for the Auth0Strategy. Note that I’ve also set up ESLint properly – I’m in a node environment (the first line). If you run eslint auth0-strategy.js, this will pass.

I also changed my callback URL. This is to allow a controller to handle the account activities. More on that later, but you will have to insert the new callback URL into the Auth0 Management console for your app.

The final piece of application code (before we get onto the controllers and views) is the server.js file:

/*eslint-env node */

"use strict";

var express = require("express"),
    http = require("http"),
    path = require("path"),
    fs = require("fs"),
    partials = require("express-partials"),
    ejs = require("ejs"),
    passport = require("passport"),
    cookieParser = require("cookie-parser"),
    session = require("express-session"),
    bodyParser = require("body-parser"),
    methodOverride = require("method-override"),
    favicon = require("serve-favicon");

/**
 * Configure the Express server to serve up the application
 * @param {express} app - the express application object
 * @return - the express application object (pipelining allowed)
 */
function configure(app) {
  // Load the server configuration file
  console.info("Loading Server Configuration");
  var config = require("./config.json");

  // Load the authentication strategy
  console.info("Loading Authentication Strategy");
  var authStrategy = require("./auth0-strategy"); // eslint-disable-line no-unused-vars

  // Set up the port that the server will listen on
  console.info("Setting Listening port");
  app.set("port", process.env.PORT || config.server.port || 3000);

  // Set up the location of the views
  console.info("Setting view location");
  app.set("views", path.join(__dirname, "views"));

  // Set Express to use the EJS view engine
  console.info("Configuring view engine");
  app.engine("html", ejs.renderFile);
  app.set("view engine", "html");

  // Set up express to use layouts with the default layout being in
  // /views/Shared/layout.html
  console.info("Configuring layout engine");
  app.set("view options", { defaultLayout: "Shared/layout" });
  app.use(partials());

  // Set up express to use the passport authentication middleware
  console.info("Configuring Passport authentication");
  app.use(cookieParser());
  app.use(session({
    secret: "app-secret",
    resave: false,
    saveUninitialized: false,
    unset: "destroy"
  }));
  app.use(passport.initialize());
  app.use(passport.session());

  // Set up static file serving
  console.info("Configuring static file serving");
  app.use("/client", express.static(path.join(__dirname, "client")));

  // Provide middleware for decoding JSON, URL-encoded body parts
  console.info("Loading Body Parser Middleware");
  app.use(bodyParser.urlencoded({ extended: false }));
  app.use(bodyParser.json());

  // Allows a controller to override HTTP verbs such as PUT or DELETE
  console.info("Loading Method Overrides");
  app.use(methodOverride());

  // Serve up a default favicon
  console.info("Configuring favicon");
  app.use(favicon(path.join(__dirname, "client/favicon.ico")));

  // Dynamically include controllers in the controllers directory
  console.info("Loading Controllers");
  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);
    }
  });

  console.info("Configuring Home Controller");
  app.get("/", function(req, res) {
    res.redirect("/home");
  });

  // Return the app so we can pipeline
  console.info("Finished configuring server");
  return app;
}

/*
 * Configure the application
 */
var server = configure(express());
http.createServer(server).listen(server.get("port"), function () {
  console.info("Express Server listening on port " + server.get("port"));
});

Phew – that’s a lot of code. Fortunately, this is all fairly explanatory with the comments that are in there. The only complicated bit is this piece of code that loads the controllers (highlighted at lines 80-89) The first block loads each javascript file in the controllers directory. For each controller, I compute a base URL. If the controller is called home.js, then the base will be /home. I’m expecting the controller to export an Express Router object – more on that when I get to controllers.

Once I’ve configured all the controllers, I set the default home page to redirect to the root document in the home controller. This is just like ASP.NET when you provide a route configuration that includes {controller=Home}.

The Basic Controller Pattern

Let’s take a look at the home controller (controllers/home.js) so that we can understand the logic that goes into it. The home controller has a home page that renders a view. However, the view only gets rendered if the user is authenticated. If the user is not authenticated, the user is redirected to the account controller:

/*eslint-env node */

"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
  };
}

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

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

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

module.exports = router;

First off, I create an Express router. I also compute the controller name based on the filename of the javascript file. Finally, I compute the loginRoute – if it is specified in the configuration, then use that, otherwise I have a default location specified.

The defaultProperties() method returns an object with some information that the layout needs. The req object is not available in the layout and I want to display my name and maybe other information from the authentication object. In addition, I want to ensure that things don’t break just because I didn’t give all the parameters the layout needs for rendering.

The index() method is the handler for the /index route. The initial part just handles redirection if the user is not authenticated. The render() statement renders a view based on the controller – In this case, this is the home controller, so it will look in views/home/index.html for its view.

The extend() statement is an interesting one and is provided by a library. It combines multiple objects together, with an order of precedence. I start with the empty object, add in the default properties, then add in the route specific properties. I suppose I could do something like:

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

As an appropriate helper. Then the index function becomes:

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

This is maybe slightly more readable, but not necessary.

The final piece of the module wires up the routes relative to the controller – I’ve given two routes, one for / and one for /index – both identical. Thus, when the user browses to /home or /home/index, they get the same page. Finally, I export the router I created. This is the object that the configuration code in server.js gets – it then links it into the main express router using the controller name as the location.

The Basic View Pattern

To go along with the basic controller, I need a basic view. All the views are relative to ./views/{controller} in this pattern. My ./views/home/index.html file is basic indeed:

<h1>Index</h1>

I want that wrapped in a layout. I included the following code in server.js to specify the default location of the layout (assuming one was not specified):

  console.info("Configuring layout engine");
  app.set("view options", { defaultLayout: "Shared/layout" });
  app.use(partials());

This tells me the layout, by default, is in ./views/Shared/layout.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="A basic web application">
  <meta name="author" content="">

  <title><%= title %> | Basic WebApp</title>

  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
  <!--[if lt IE 9 ]>
    <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
    <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
  <![endif]-->

  <link rel="stylesheet" href="/client/layout.css">

</head>
<body>
  <div id="wrapper">
    <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
      <div class="navbar-header">
        <a class="navbar-brand" href="/">Basic WebApp</a>
      </div>
      <!-- Top Menu Items -->
      <ul class="nav navbar-right top-nav">
        <li><a href="/account/profile"><%= user.displayName %></a></li>
        <li><a href="/account/logout">
          <i class="fa fa-sign-out"></i>
          <span class="sr-only">Sign Out</span>
        </a></li>
      </ul>
    </nav>
    <section id="page-wrapper">
      <div class="container-fluid">
        <%- body %>
      </div> <!-- /.container-fluid -->
    </section> <!-- /#page-wrapper -->
  </div> <!-- /#wrapper -->

  <script src="http://code.jquery.com/jquery-2.1.3.min.js"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
</body>
</html>

I’ve included a default navbar that includes the user display name and a link to the logout. I have not created an /account/profile in this article (and there isn’t going to be one – that’s for you to write). Note that I’ve also added in some CSS (which I’m not going to show off in the article – go check out the GitHub Repository).

This is very similar to the Razor _Shared.cshtml file in ASP.NET. The rendering is a little different due to the change in rendering engine, but it should feel familiar.

The Account Controller

In ASP.NET, the Account Controller was special. It’s no different here. I’m only doing Social Authentication in this version. In the Auth0 management portal, I’ve turned on Twitter, Facebook, Windows Live and Google authentication. I have not changed the API keys from the Auth0 development keys. If you were deploying this in production (and paying Auth0), then you would definitely want to register your own keys with Auth0.

If you click on your app and then on Connections (along the top), you should see something like this:

blog-code-0521-1

Now that is done, let’s take a look at the controller controllers/account.js:

/*eslint-env node */

"use strict";

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

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

/**
 * Build a URL for a specific provider based on the configuration and
 * the provider name
 */
function buildurl(provider) {
  var server = config.server.uri || "http://localhost:3000";

  var url = "https://" + config.passport.auth0.domain + "/authorize" +
    "?response_type=code&scope=openid%20profile" +
    "&client_id=" + config.passport.auth0.clientid +
    "&redirect_uri=" + server + "/" + controller + "/external-callback" +
    "&connection=" + provider;

  return url;
}

Up to this point, it’s a regular controller. I’ve defined a private function for building a URL to link into the Auth0 system. I’m going to pass these URLs as locals in the login method – speaking of which:

/**
 * GET /{controller}/login
 */
function login(req, res) {
  var locals = {
    layout: false,
    connections: {}
  };
  var connections = config.passport.auth0.connections || [];
  for (var i = 0; i < connections.length; i++) {
    locals.connections[connections[i].replace("-","_")] = buildurl(connections[i]);
  }

  res.render(controller + "/login.html", locals);
}

/**
 * GET /{controller}/logout
 */
function logout(req, res) {
  req.logout();
  res.redirect("/");
}

// Wire up Per-route functionality
router.get("/login", login);
router.get("/logout", logout);

This section is all about wiring up the login and logout routes. The login route will display the ./views/account/login.html view, and I am passing in the list of connections from the config.json file. You can have as many as you want here.

// Social Identity callback - set this in the Auth0 Manage App page
router.get(
  "/external-callback",
  passport.authenticate("auth0", {
    failureRedirect: "/" + controller + "/failure-callback"
  }),
  function (req, res) {
    if (!req.user) {
      throw new Error("user is null");
    }
    res.redirect("/");
  }
);

module.exports = router;

Finally, we need to handle the callback. This is set in the auth0-strategy file and this file – in two places. Firstly, in the buildurl() method and then again in the route to handle the external callback. You can specify a new route /failure-callback that displays a “Oh Noes – Something bad happened” view, or you can just let it 404. If everything is successful, we redirect back to the home page – this time authenticated.

The ./views/account/login.html is a complete HTML page. I’ve specified “layout: false” in the locals – a signal to the rendering engine to not use a layout file:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">

  <!-- Bootstrap, Bootstrap-Social, Font-Awesome -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">

  <!-- My minimal stylesheet -->
  <link rel="stylesheet" href="/client/bootstrap-social.css">
  <link rel="stylesheet" href="/client/login.css">
  <title>Login | Basic WebApp</title>
</head>
<body>
  <div id="outer">
    <h2>Login with your Social Provider</h2>
    <div class="row">
      <div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 social-buttons">
        <a class="btn btn-block btn-social btn-twitter" href="<%= connections.twitter %>">
          <i class="fa fa-twitter"></i> Sign in with Twitter
        </a>
      </div>
      <div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 social-buttons">
        <a class="btn btn-block btn-social btn-facebook" href="<%= connections.facebook %>">
          <i class="fa fa-facebook"></i> Sign in with Facebook
        </a>
      </div>
      <div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 social-buttons">
        <a class="btn btn-block btn-social btn-microsoft" href="<%= connections.windowslive %>">
          <i class="fa fa-windows"></i> Sign in with Microsoft
        </a>
      </div>
      <div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 social-buttons">
        <a class="btn btn-block btn-social btn-google" href="<%= connections.google_oauth2 %>">
          <i class="fa fa-google"></i> Sign in with Google
        </a>
      </div>
    </div>
  </div>
</body>
</html>

I’ve downloaded bootstrap-social.css and put it in my client static area for this.

With all this code, I now have a complete basic web app that authenticates via one of four social logins and produces a nice Bootstrap-enabled front page.

You can get all the code from my GitHub Repository.