This Small Corner

Eric Koyanagi's tech blog. At least it's free!

Building an MVC app with Express, Sequelize, Tailwind, and Quill

By Eric Koyanagi
Posted on

Express Framework vs. Laravel

Working with Laravel is (often) a joy because the framework is opinionated while still being somewhat flexible. That opinionation gives us a huge swatch of out-of-the-box functionality: a solid MVC structure, an ORM, validation, error handling, and a very small amount of boilerplate required to write code. 

Node’s most popular framework, Express, isn’t so opinionated. It relies on the author to understand what disparate packages to use and how to structure the project. This flexibility can often be a double-edged sword. More than that, it demands that I research (and trust) even more third party packages to make a robust structure instead of the framework authors doing the work for us. Of course there's many advantages to the framework being flexible and un-opinionated, but overall...? Laravel is the more robust and full-featured framework, without a doubt.

That's said, let’s dive into one approach for structuring an express application. For those that are experienced with the framework, this will be very boring. That said, it might help people that are more used to Laravel's MVC structures and are confused when they bootstrap an Express application.

Specifically, we’re going to make controllers (which express doesn’t really have by default), models, interfaces (yes, interfaces in JavaScript!), and some service classes.

Unlike the Laravel app, we’ll build these services around interfaces to make it even easier to implement multiple page builders or uploaders (e.g. to upload to someplace other than S3). We'll also use Tailwind for the front end, which will require some changes to the WYSIWYG editor implementation. Let's get started! 


The Goal: A Simple, MVC Express Implementation

We’ll be using a variety of tools and packages to create our application. First, we’ll use the express application generator to scaffold the project. 

In the main project, create folders for controllers and models. Before we understand how to build controllers, let’s look at some of the other packages we’ll be using.

  • dotenv for environment variables 
  • express-async-handler to reduce boilerplate around try/catches (another example where having a more opinionated framework would help!)
  • express-validator for form validation
  • sequelize for our ORM (there’s ample tutorials around Mongoose already and I need to connect to an existing mySQL DB, not Mongo)
  • pug as our view engine (FKA Jade)  

The general goal is to have lightweight controllers that help us orchestrate between models and views, keeping our routes very clean and making it easier to understand application flow.


Setting up Sequelize with Express

First, let’s make sure we can connect to the existing mySQL DB we created with the Laravel project. I will do this by creating a new “config” folder, where I’ll dump my database configuration options. This isn’t really required, but it does ensure that all our DB options are in one easy-to-find location. 

Then, we’ll create an index.js file in our models folder. This isn’t something I cooked up myself, be sure to read the article here for more details (including a db.config.js example) -- or use the Sequelize CLI to bootstrap this instead: 

const dbConfig = require("../config/db.config.js");

const Sequelize = require("sequelize");
const sequelize = new Sequelize(dbConfig.DB, dbConfig.USER, dbConfig.PASSWORD, {
  host: dbConfig.HOST,
  dialect: dbConfig.dialect,
  pool: {
    max: dbConfig.pool.max,
    min: dbConfig.pool.min,
    acquire: dbConfig.pool.acquire,
    idle: dbConfig.pool.idle
  }
});

const db = {};

db.Sequelize = Sequelize;
db.sequelize = sequelize;

db.articles = require("./article.js")(sequelize, Sequelize);

module.exports = db;

Unfortunately, we need boilerplate like this because we have to pass the sequalize(instance) and Sequalize(class) references to each model. In our app.js/server.js (wherever you put your main express bootstrap), we can easily load all our models like this: 

// database setup
const db = require("./models");
db.sequelize.sync()
  .then(() => {
    console.log("Synced db.");
  })
  .catch((err) => {
    console.log("Failed to sync db: " + err.message);
  });

This might look very different if you're used to Mongoose, which doesn't require this model "boostrapping". It’s up to you if you want to call “sync”, here, as that might not be a good idea on prod. For our example, it’s fine. Don’t forget to include your dotenv initialization in the same place: 

require('dotenv').config() 

That’s it! We’re ready to use Sequelize now, so the next step is understanding how to structure our controllers, which will leverage these models. 

How to Make Controllers in Express

This isn’t that complicated. A controller just defines some action that is attached to some route. Let’s start in our routes file (index.js is fine): 

// Article Routes
const article_controller = require("../controllers/articleController");

// List articles
router.get("/", article_controller.article_list);

Easy, right? First we require any and all controllers -- for an application with many controllers, we’d maybe want this separated into distinct route files for better organization. Then we define each route, supplying the controller action to use for that route. 

In other words, the controller is just a way to put the “meat” of a route into an easy, maintainable structure instead of stashing everything in the route file. That’s really it. The route file does only what it should: define routes! 

As you can imagine, the controller implementation itself is not that complex, either:

const db = require("../models");
const Article = db.articles;
const asyncHandler = require("express-async-handler");
const { body, validationResult } = require("express-validator");

// List of articles for editing
exports.article_list = asyncHandler(async (req, res, next) => {
    const data = await Article.findAll({ order: [['id', 'DESC']] });
    res.render("index", { title: "Articles", articles: data });
});

Here, we include a few utilities like express-validator (for form validation later on) and asyncHandler. The description of asyncHandler is “ Simple middleware for handling exceptions inside of async express routes and passing them to your express error handlers”.

Since our controllers are in fact defining actions for routes, we can use asyncHandler to eliminate a lot of annoying try/catch boilerplate that would otherwise be required to properly handle errors. 

Otherwise, the controller does what we expect. It uses the model to obtain all articles, then passes that list to a view for rendering. Between the controller and the model, we can keep code very well organized. Our business logic will live in models or service classes, the controllers will orchestrate between models and views, and we have a foundation that's easy to expand and maintain.

With very little effort, we already have a much better structure than what Express gives us by default. 


CRUD and Beyond

We see how we can use controllers to keep our code organized and focused and how an ORM reduces the boilerplate around CRUD operations. Still, it’s worth looking at the create and update facets of CRUD specifically because it’s very common to want to do the following: “either make a new instance of a model or update an existing one.”

Fortunately, it’s very easy to make a single controller action and matching view that can both create and update…it just isn’t well explained in most tutorials. 

This is my create and update action for the article controller: 

exports.article_create_post = [ 
    // express-validator middleware
    body("title", "Title must contain at least 3 characters")
        .trim()
        .isLength({ min: 3 })
        .escape(),
    body("body", "Content must contain at least 100 characters")
        .trim()
        .isLength({ min: 100 }),
    body("author", "Author can't be blank")
        .notEmpty()
        .trim()
        .escape(),

    asyncHandler(async (req, res, next) => {
        // Extract the validation errors; either create/update or re-render the form with errors
        const errors = validationResult(req);
        if (errors.isEmpty()) {
            const article = await Article.upsert({
                title: req.body.title,
                body: req.body.body,
                author: req.body.author,
                published: req.body.published,
            }, [{id: req.body.id}])

            await article.publish();    

            res.render("articleForm", { title: "Create or Edit Article", article: article, saved: true });
        } else {            
            res.render("articleForm", { title: "Create or Edit Article", article: req.body, errors: errors });
        }
    })
];

Note that the repo has a more up-to-date version of this code; I will update these examples once the project is entirely completed

First, note that this action is an array; we can easily use this to arrange all our route middleware, such as express-validator as shown. This is a general pattern we can use for any form submit. 

This makes validation very easy. That said, maybe I would prefer to have this defined on a model level instead of the controller. It’s easy to imagine how lengthy controllers can become in a complex app. 

Controller code is meant to be high level and easy to read. We can follow this code easily and see how it validates, and if there are no errors then upserts the article. Finally, we see how it calls a “publish” method on the model to upload the content to S3 (or whatever data store you want). The implementation details are neatly obfuscated within specialized classes.

One other thing to note is that “upsert” isn’t a native method in Sequelize (I think it should be). We define this on our article model as a class function like this:

  Article.upsert = function(values, condition) {    
    return Article
      .findOne({ where: condition })
      .then(function(obj) {
          if(obj)
            return obj.update(values);

          return Model.create(values);
      })    
  };

This simply finds and updates a model, or creates a fresh instance if there isn’t an existing one. It’s similar to “findOrCreate”, but is more like “findAndUpdateOrCreate”, which is easier for this use case.

The publish method is what actually builds a static HTML version of an article and uploads content to S3. Before we can look into those details, we should talk about interfaces. 


Interfaces in Express

Our app is conceptually simple: we have an article model and controller, and when articles are saved, HTML versions are created and uploaded to S3. Thinking about this more abstractly, the article model needs two generic services: one that builds an HTML representation of content, and another that publishes that content somewhere. 

By building interfaces, we can lay solid foundations so that we can easily expand the types of content or publish that content any way we want. If an interface is just a “contract” to ensure that we have to implement certain methods, the way to do that in Express is simple enough:

class BlogPublisherInterface {
  constructor() {
    if(!this.publish) {
      throw new Error("Blog publishers must have a publish method");
    }
  }
}
module.exports = BlogPublisherInterface

All we do is check to see if methods are defined and throw an error if they aren’t. To implement the interface, we extend from this class, like so:

const BlogPublisherInterface = require('../interfaces/BlogPublisherInterface');
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); 

class S3Publisher extends BlogPublisherInterface {
  async publish(fileName, content) 
  {  
    const client = new S3Client({ region: 'us-east-1' });
    const input = {
      Body: content,
      Bucket: process.env.AWS_BUCKET, 
      Key: fileName,
    };

    const command = new PutObjectCommand(input);
    return await client.send(command);
  }
}
module.exports = new S3Publisher()

In this case, the publisher uploads a file to S3, but I can easily add or change this. Since I’ve coded against an interface, I know that changing this implementation won’t be very hard. 

Building this logic into service classes also keeps our model lean. Maybe you've heard of "thin controllers, fat models" as a philosophy before, but we don't really want fat models, either! Also, publishing isn't specific to an article alone, so these service classes decouple this operation from "articles", improve maintainability, and keep the model lean and readable. If I had a "Video" model in addition to articles, the use of service classes can help us re-use this code without having to lift all that logic into the controller where it will become messy.


Return HTML for Rendered View & Use Multiple Layouts

The last "major" piece to our application is how it creates static files given a piece of content. Our interface allows us to create multiple content types so that we can make an “article detail” page separate from the “article list view” homepage…or more pages beyond that. 

There are a few caveats to how views work that are worth remembering. First, it will use the layout (called “layout.pug”) specific to any subfolder before using a more generic one. Second, we can obtain the actual HTML for a rendered view with a callback, which we can use to upload to S3. 

Here’s an example of the “build the article detail page” implementation: 

const PageBuilderInterface = require('../interfaces/PageBuilderInterface');
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); 
var path = require('path');
const express = require('express')

class ArticleDetailBuilder extends PageBuilderInterface {
  async setContent(data) 
  {  
    this.data = data;
  }

  buildPage(publisher) 
  {
    var appInstance = express();
    appInstance.set('views', [path.join(__dirname, '../views/rendered-article'), ]); 
    appInstance.set('view engine', 'pug'); 

    appInstance.render("article", { article: this.data }, (err, html) => {
      if (err) {
        console.log("Render error", err)
      }

      publisher.publish(this.data.slug, html)
    });
  }
}
module.exports = new ArticleDetailBuilder()

You can see how we have to define the views paths and engines here, but that gives us the opportunity to define it as a subfolder instead of using the root “views” folder. This allows us to easily use multiple layouts in express. We use a relative directory here because this specific class lives in a “services” subfolder off the application root. 

However you manage the subdirectory, the “views” var must contain the specific path for the desired view or it will fail to render. 

This way, we can easily keep our layouts and front end separate. The base folder is for our admin UI, the app that drives the blog edits. The subfolders are used for actual public display on S3 and can share some elements or use entirely different elements for the index/detail views. 

Self-Associations in Sequelize

One thing that the docs could make more clear is how to relate a model to itself with Sequelize. We need this for our next and previous links. The docs show a variety of examples on one-to-one relations, but only when those relations exist on different models. How do we create associations on the same table in Sequelize?

Article.belongsTo(Article, { foreignKey: 'next_id', as: 'nextArticle' });
Article.belongsTo(Article, { foreignKey: 'previous_id', as: 'previousArticle' }); 

The answer is that we only provide the "belongsTo" method. The foreign key is simply the column that contains the ID of the related object. Since we have two of these associations, we must use an alias, too.

Honestly, I'm not a huge fan of Sequelize after using it for a few reasons. The docs aren't especially great, as we see above, and are incomplete in other ways. For example, I don't think the ORM should impose restrictions on how we name columns (within the rules of the DB engine of course), but Sequelize does. Name a column "previous" and you won't get any errors in the schema or when trying to query against it...and examining the model will even show the right data returned. Try to access that data and it will fail, though, because "previous" is the name of a function on all model instances. I would at the very least appreciate a simple list of all column names that are invalid with this ORM....but that doesn't seem to exist. Otherwise, they could have picked names less likely to collide with existing columns.

Further, it's a bit annoying that the instructions for setting up migrations assume you're starting with a blank project. Why did they choose to name the config file for this something as generic as "config/config.json"? Even more silly...if you already have something like a models folder with an index.js defined, it will throw an error. Your only choice is to overwrite that file. Their own docs don't do a good job (at all) of insisting that you bootstrap the project with their CLI if you want to use migrations. There's also no reason to force an overwrite if such a file already exists...just skip it and continue instead of forcing me to fix the issue myself.

There's yet another flaw with this ORM and its (lack of) documentation, which is that it only shows examples on how to create associations when you define models within the same file, which isn't a normal use case. This would be fine if the models were used in an intuitive way, but remember that we have to initialize the models with sequelize references in models/index.js, which means we can't simply define the associations in the model itself. The only example I could find from them dumps this into an "extra setup" step...which, they again didn't bother to name this something specific, since all it does it create associations.

In summary, I'm very unimpressed with Sequelize as an ORM. It offers us no clean way to define associations and requires extra boilerplate just to initialize all our models. Further, the docs are clearly lacking, which I admit is somewhat normal for many projects, but not at this level. Not when the docs show contrived examples that do not reflect anything real-world even for very basic things like managing associations!

If you do want to use Sequelize, do it in a fresh project and be sure to use the CLI to bootstrap it. Would I be eager to use this ORM in a real, production environment? Probably not. This is one of the challenges in working with unopinionated frameworks like Express. You might not find a package or library that does what you need to do in a robust way like you would with a framework-default ORM like Eloquent.

All this being said, I don't think the issues are so serious that I'm tempted to rip it all out. It's entirely possible to use an ORM you don't really like and still get more benefit from it than it costs in frustration or work-arounds. All things considered, it's still probably one of the better options with Node and mySQL.


Customizing Quill to Use Tailwind Classes

As part of our blog, we naturally have a WYSIWYG editor, powered by Quill in this case. However, these editors love inline styles and semantic classes. How can we make Quill work with something like Tailwind? There's packages that offer out-of-the-box "solutions", but I want granular control so that I can really customize the presentation. To do this, I created a quillConfig.js file that handles initializing the widget with all options and customizations. We can somewhat easily extend Quill features to add CSS classes:

var CodeBlock = Quill.import('formats/code-block'); 
class CustomCodeBlock extends CodeBlock {
 static create() {
  const node = super.create();
  node.classList.add('bg-black');
  node.classList.add('shadow-sm');
  node.classList.add('rounded-xl');
  node.classList.add('p-3');
  node.classList.add('my-1');
  node.classList.add('text-white');
  node.classList.add('overflow-auto');
  return node;
 }
}

var Header = Quill.import('formats/header'); 
class CustomHeader extends Header {
 static create(headingLevel) {
  const node = super.create(headingLevel);   
  const headingClasses = {
   1: 'text-2xl',
   2: 'text-xl',
   3: 'text-lg',
   4: 'text-lg',
   5: 'text-base',
   6: 'text-base'
  };

  node.classList.add(headingClasses[headingLevel]);
  node.classList.add('text-cyan-600');

  return node;
 }
}

Here we see two examples that add custom classes. The first is the code block; this still is fairly simple without special syntax highlights (yet), but it does wrap it in familiar tailwind classes to improve the presentation. We also show the header element (because I didn't see a lot of great examples on this one), which shows how you can add custom classes to a header based on its header level. The last thing is to register these extended components like this:

// Register our custom blocks
Quill.register(CustomBlock, true);
Quill.register(CustomCodeBlock, true);

Author Block, Migrations and Sitemap Generator

Alright, back to some backend shenanigans. We already built the concept of a page publisher, which makes the sitemap generator effortless. We already have implemented a PageBuilder for our article list page which collects a list of all articles and builds a page...so that's an obvious place to stick a sitemap generator, too:

sitemapBuilder.buildSitemap(this.data, publisher);

The underlying method in our "sitemapBuilder" class is almost as simple:

buildSitemap(articles, publisher) 
{  
   const sitemapXml = this.getXML(articles);
   publisher.publish("sitemap.xml", sitemapXml);
}

This first line uses the easy XML package to create an XML string for each article, which it passes to the publisher for easy upload. Great!

We'll add a Sequelize migration to add a new table for "authors" so we can insert an "author block" (see the end of this article), a common feature that can serve as a pattern for other re-usable content blocks (like "related articles"). This uses relations that we've already seen, but it's actually an easier use case since it isn't a "self-association" like our previous/next links. One thing to note here is that we will need to change our "upsert" Article method if we want to eagerly load associations:

return Article
   .findOne({ where: condition, include: 'author' })
   .then(function(obj) {
     if(obj)
      return obj.update(values);

     return Article.create(values);
   })

What's Next...?

You can see the complete source code for this project here

Our express app is now an MVC-style application with a robust ORM, validation, and controllers. It has migrations, a sitemap builder, and a flexible system of publication that can be swapped to add more page types or publish to different content stores. We've integrated with a WYSIWYG editor, although it's rather imperfect and annoying (more on that later?), and we use Tailwind for styling.

However, it's still missing tests and database seeds! We'll look into that next, but this article has to stop...thanks for reading! (it's okay if you just skimmed it, too)

« Back to Article List
Written By
Eric Koyanagi

I've been a software engineer for over 15 years, working in both startups and established companies in a range of industries from manufacturing to adtech to e-commerce. Although I love making software, I also enjoy playing video games (especially with my husband) and writing articles.

Article Home | My Portfolio | My LinkedIn
© All Rights Reserved