Skip to content

MVC Organisation with Express and Thinky

Posted on:January 17, 2016

I wrote an example to demonstrate a way to organise your Express app in a MVC manner without resorting to complicated frameworks.

Check it out here.

This particular example is as barebones as possible. In particular, there’re no explicit view layer to speak of - we’re simply returning JSON. We could have implemented, for example, server-side rendered React/Redux into this example, and I intend to do that in as a separate example.

There are a few concepts I want to talk about here:

index.js

First, we import babel-polyfill so that we can use async/await, which greatly simplifies working with promises and is the main reason behind the leanness of the code.

import "babel-polyfill";

Then, we import express and body-parser to create the main Express object, and to parse the body of POST requests that are sent in either x-www-urlencoded or JSON format.

import express from "express";
import bodyParser from "body-parser";
const app = express();
 
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

Then, we require the config file, which contains the connection details for RethinkDB. We inject the config object as a dependency to schema.js, which creates the connection to RethinkDB and bootstraps our database schema and table objects. I will be using this pattern a lot more throughout the example.

const config = require("./config");
const models = require("./schema")(config);

schema.js

In schema.js, we define the entire schema, just like schema.rb in Rails. I will defer the details of the syntax to thinky’s documentation. Note that this file can swapped out for any other ORM if you want (Sequelize, etc).

After the schema is defined, we pass the thinky database objects to the model files:

const allModels = { User, Post, Category };
 
return {
  User: require("./models/User")(r, allModels),
  Post: require("./models/Post")(r, allModels),
  Category: require("./models/Category")(r, allModels),
};

As a matter of convenience, I chose to pass all the models so that model methods can reference other database objects as well.

Model Methods

Model methods serve as a way to encapsulate ORM logic and make it reusable. Even though ORMs themselves already offer a layer of abstraction on top of the database, I prefer having model methods as an additional layer to meet common needs in your application’s business logic. Model methods are also handy in case you ever want to swap out the ORM for another one down the road.

As an example, let’s look at the User model:

module.exports = (r, models) => {
  const { User } = models;
  return {
    find: function (id) {
      const user = User.get(id);
      return user;
    },
 
    create: function (params) {
      const { id, username, email } = params;
      const newUser = new User({ id, username, email });
      return newUser.save();
    },
  };
};

A user has find and create methods, which behaves like User.find and User.create in Rails. The find method is simply a thin wrapper around the User.get thinky method, while create wraps around the new User and save thinky calls. This is especially notable because using constructor functions (new) is generally a bad idea (a questionable design decision on thinky’s part), but wrapping it around our create model method limits the potential damage it can cause.

Routing

Back to index.js, the routing is initialized by passing in the Express object and our models.

require("./routes")(app, models);

The routes are found in routes.js, and is designed in a way as to mimic routes.rb functionality in Rails. I discuss further about the design behind this routing here. One defines “top-level” resources in the routes constant - any routes defined in their corresponding router files will be mounted on the root. In this particular example, we have two top-level resources: users and categories.

const routes = ["users", "categories"];

Resources that are logically nested within another resources are defined in the nestedRoutes constant as a Map. Here, posts is a resource nested under users.

const nestedRoutes = new Map();
nestedRoutes.set("users", ["posts"]);

Let’s look at the User controller:

module.exports = (router, models) => {
  const { User } = models;
  router.post("/", async (req, res) => {
    try {
      const newUser = await User.create(req.body);
      res.json(newUser);
    } catch (e) {
      res.sendStatus(422);
    }
  });
};

This particular endpoint is mounted on POST /users and is used to create a new user. As you can see, we destructure models and assign User, and call the create model method with the request’s body. We then respond with create’s return value, which is the information of the newly-created user. If an exception is thrown during create, then we return a 422 to indicate an unprocessable entity.

Conclusion

That pretty much sums up the entire “framework”. As I mentioned, this is a very barebones example, but it serves as a solid base for a minimal MVC Express app. I may expand on this example with examples of custom middleware and actual explicit views (e.g. React).