Express like it's ROR

Dec 10 2016

Ruby on rails supplies a big tool set that allows rapid development right out of the box. As a ROR developer, you might find Express too minimal. But with some tweaking and configurations you'll feel right at home.

Setup

Assuming you've done  installations  right, creating a new project is pretty easy:

  express myapp


This will create a new project under the  myapp  folder. The two main files are:  
app.js  - This creates the express app.  
bin/www  - This runs the app in a server.

ORM configurations

There are a few ORMs you can choose from; the most popular is Sequelize.

  npm install sequelize sequelize-cli pg pg-hstore  --save
  node_modules/.bin/sequelize init


This will create the config/config.json (db configurations, very much the same as rails database.yml), as well as folders, such as:

  • migrations  
  • seeders  
  • models

Rails offers  rake db:create/drop. Sequelize unfortunately does not supply such functionality.

We'll need to setup our DB. In our case, we'll use Postgres in other ways. For example, via the terminal:

  createdb sequlize_development


Next, we'll need to connect the ORM to our app. We'll then edit our  bin/www  file so the server will run only after the DB connection is established by changing:

  // bin/www
  server.listen(port);
  server.on('error', onError);
  server.on('listening', onListening);


To:

  // bin/www
  var db = require('../models');


  db.sequelize.sync().then(function() {
    server.listen(port);
    server.on('error', onError);
    server.on('listening', onListening);
  });


We need to make sure Sequelize knows how to access our new DB.  
Here, I'm assuming our postgres username and password are both 'postgres':

  // `config/config.js`
  {
    "development": {
      "username": "postgres",
      "password": "postgres",
      "database": "sequlize_development",
      "host": "127.0.0.1",
      "dialect": "postgres"
    },
    "test": {
      "username": "root",
      "password": null,
      "database": "sequlize_test",
      "host": "127.0.0.1",
      "dialect": "postgres"
    },
    "production": {
      "username": "root",
      "password": null,
      "database": "sequlize_production",
      "host": "127.0.0.1",
      "dialect": "postgres"
    }
  }


Migrations

Sequelize-cli supplies quick models/migrations generators just like  rails generatecommand"

  node_modules/.bin/sequelize model:create --name Employee


will create  models/employee.js  &  migrations/TIMESTAMP-create-employee.js.

  //migrations/TIMESTAMP-create-employee.js;
  module.exports = {
    up: function(queryInterface, Sequelize) {
      return queryInterface.createTable('Employees', {
        id: {
          allowNull: false,
          autoIncrement: true,
          primaryKey: true,
          type: Sequelize.INTEGER
        },
        name: {
          type: Sequelize.STRING
        },
        active: {
          type: Sequelize.BOOLEAN
        },
        createdAt: {
          allowNull: false,
          type: Sequelize.DATE,
        },
        updatedAt: {
          allowNull: false,
          type: Sequelize.DATE
        }
      });
    },
    down: function(queryInterface, Sequelize) {
      return queryInterface.dropTable('Employees');
    }
  };


  // models/employee.js
  'use strict';
  module.exports = function(sequelize, DataTypes) {
    var Employee = sequelize.define('Employee', {
      name: DataTypes.STRING,
      active: DataTypes.BOOLEAN,
    }, {
      classMethods: {
        associate: function(models) {
        }
      }
    });
    return Employee;
  };


Now, we can run our migrations, and roll them back:

  `node_modules/.bin/sequelize db:migrate`
  `node_modules/.bin/sequelize db:migrate:undo`


DB relations

Relations are similar to Rails. We need to do two things:  
1. Add a secondary key to the DB table  
2. Add relation deceleration to the model

Let's add Office & Pets models and an employee-belongs-to-office, pet-belongs-to-employee & employee-has-many-pets relations:

  // migrations/TIMESTAMP-create-office.js
  module.exports = {
    up: function(queryInterface, Sequelize) {
      return queryInterface.createTable('Offices', {
        id: {
          allowNull: false,
          autoIncrement: true,
          primaryKey: true,
          type: Sequelize.INTEGER
        },
        name: {
          type: Sequelize.STRING
        },
        active: {
          type: Sequelize.BOOLEAN
        },
        createdAt: {
          allowNull: false,
          type: Sequelize.DATE
        },
        updatedAt: {
          allowNull: false,
          type: Sequelize.DATE
        }
      });
    },
    down: function(queryInterface, Sequelize) {
      return queryInterface.dropTable('Offices');
    }
  };


  // migrations/TIMESTAMP-add-officeid-to-employee.js
  module.exports = {
    up: function (queryInterface, Sequelize) {
      queryInterface.addColumn(
        'Employees',
        'OfficeId',
        Sequelize.INTEGER
      )
    },


    down: function (queryInterface, Sequelize) {
      queryInterface.removeColumn('Employees', 'OfficeId')
    }
  };


// migrations/TIMESTAMP-create-pet
  module.exports = {
    up: function(queryInterface, Sequelize) {
      return queryInterface.createTable('Pets', {
        id: {
          allowNull: false,
          autoIncrement: true,
          primaryKey: true,
          type: Sequelize.INTEGER
        },
        name: {
          type: Sequelize.STRING
        },
        active: {
          type: Sequelize.BOOLEAN
        },
        EmployeeId: {
          type: Sequelize.INTEGER
        },
        createdAt: {
          allowNull: false,
          type: Sequelize.DATE
        },
        updatedAt: {
          allowNull: false,
          type: Sequelize.DATE
        }
      });
    },
    down: function(queryInterface, Sequelize) {
      return queryInterface.dropTable('Pets');
    }
  };


  // models/pet.js
  module.exports = function(sequelize, DataTypes) {
    var Pet = sequelize.define('Pet', {
      name: DataTypes.STRING,
      active: DataTypes.BOOLEAN,
      EmployeeId: DataTypes.INTEGER
    }, {
      classMethods: {
        associate: function(models) {
          Pet.belongsTo(models.Employee)
        }
      }
    });
    return Pet;
  };


  // models/employee.js
  'use strict';
  module.exports = function(sequelize, DataTypes) {
    var Employee = sequelize.define('Employee', {
      name: DataTypes.STRING,
      active: DataTypes.BOOLEAN,
      OfficeId: DataTypes.INTEGER
    }, {
      classMethods: {
        associate: function(models) {
          Employee.belongsTo(models.Office);
          Employee.hasMany(models.Pet);
        }
      }
    });
    return Employee;
  };


Seeds

Sequelize seeds entail a somewhat different approach than Rails standards. They look and behave like migrations.

One of the most annoying things about sequelize seeds is that they interact directly with your DB, and not the model. So, if we define default values at our model  they won't be set by the seed.  

In our example, the null values at  createdAt  &  updatedAt  will raise exceptions during the seed execution.  This is why we must manually define the  now  variable at our seeds below.

Let's create seeds for employees and offices:

  node_modules/.bin/sequelize seed:create --name employees
  node_modules/.bin/sequelize seed:create --name offices


  // seeders/TIMESTAMP-employees.js
  function createEmployee(index) {
    var now = new Date();
    return { 
      active: Math.random() > .5, // randomize active == true OR false
      name: "name-"+index,
      createdAt: now,
      updatedAt: now
    }
  }


  function generateEmployees () {
    var i;
    var employees = [];
    for (i = 0; i < 100; i++) {
      employees.push(createEmployee(i));
    }
    return employees;
  }


  module.exports = {
    // insert 100 employees to db
    up: function (queryInterface, Sequelize) {
      return queryInterface.bulkInsert('Employees', generateEmployees(), {});
    },


    // delete ALL employees from db
    down: function (queryInterface, Sequelize) {
      return queryInterface.bulkDelete('Employees', null, {});
    }
  };




We can run the seeds one by one:

  node_modules/.bin/sequelize db:seed --seed seeders/TIMESTAMP-employees.js
  node_modules/.bin/sequelize db:seed --seed seeders/TIMESTAMP-offices.js


Or all by the order they were created:

  node_modules/.bin/sequelize db:seed:all


And we can roll them back:

  node_modules/.bin/sequelize db:seed:undo --seed seeders/TIMESTAMP-employees.js
  node_modules/.bin/sequelize db:seed:undo:all


Customized seeds

Personally, I like seeds that are 'standalone', and are not affected by other seeds in our system, or the creation order. This is why usually I add a setup such as  this  to my rails apps.

Such setup in Sequelize can be simply defined by node scripts.  
Another plus is that we use our models logic, so default values & validations will behave as expected.

// seeders_custom/seed_for_dev.js
var db = require('../models/index');


function createEmployee(index) {
  return { 
    active: Math.random() > .5, // randomize active == true OR false
    name: "bulk-name-"+index
  }
}


function generateEmployees () {
  var i;
  var employees = [];
  for (i = 0; i < 100; i++) {
    employees.push(createEmployee(i));
  }
  return employees;
}


var employees = generateEmployees();


var offices = [
  { 
    active: true,
    name: "office 3"
  },
  { 
    active: true,
    name: "office 4"
  }
]


var pets = [
  { 
    active: true,
    name: "lucky"
  },
  { 
    active: true,
    name: "flipper"
  }
]


db.Employee.destroy({where: {}}).then(function () {});
db.Office.destroy({where: {}}).then(function () {});


db.Employee.bulkCreate(employees);
db.Pet.bulkCreate(pets);
db.Office.bulkCreate(offices);


db.Office.findOne({where: { name: 'office 3' }})
  .then(function(office) {
      return db.Employee.update(
        { OfficeId: office.id },
        {where: {active: {$eq: true}}}
      )
  })


db.Employee.findOne({where: { name: 'bulk-name-1' }})
  .then(function(employee) {
      return db.Pet.update(
        { EmployeeId: employee.id },
        {where: {active: {$eq: true}}}
      )
  })


Queries

Queries over a single model are pretty straight foreword:

  var db = require('../models')
  db.Office.findOne({
    where: {name: 'office 3'}
  })


But queries over multiple tables can lead to:  

  • inefficient DB access such as N+1 queries  
  • Callback hell.

Sequelize  eager loading  can simplify such queries:

  // all the pets that belongs to employees from `office 3`
  var db = require('../models')
  db.Pet.findAll({
      include: [{
        model: db.Employee,
        include: [{
          model: db.Office,
          where: {
            name: "office 3"
          },
          required: false
        }]
      }]
    }).then(function(pets) {
      JSON.stringify(pets)
      });
    })


Another way to simply such queries is to force synchronous execution by using ES7 async/await. This is described in this blog post.

Debugging

Setting up debuggers with Rails is very easy. Just drop in  byebug  or  pry  gems into your Gemfile and you're up and running.

With Express, this requires some more meddling. Some IDEs such as Visual Code & WebStorm supply their own debuggers setup,  
but I prefer all my team members to be able to debug their code without triggering an IDE war at the office.

Node Inspector

Node-inspector allows you to use Cchrome dev tools on server code.  
If you're a front end developer, you'll be right at home.  

Basically, a node-inspector observes the node app we wish to debug, and allows us to interact with it during run time.

This  video  covers the setup. In short, it is something like this:

  npm install node-inspector --save-dev
  # run node inspector
  ./node_modules/.bin/node-inspector bin/www
  # run server in debug mode
  node --debug bin/www


Now, every break point (or  debugger  command in the code) will pause the server execution. We can view the dev tools under:  http://127.0.0.1:8080/?port=5858

Node Debug

Pausing the code at run time and viewing the call stack is nice,  
but what if we want to dig deeper and manually execute commands while in a break point?  

Node debug allows you to use console while debugging by triggering Repeat-Eval-Print-Loop;

  node debug bin/www


  • Note that by default, it stops on the first line of code. So hit  c  to continue.  
  • When reaching a break point type  repl  to allow terminal interaction.

Live Reload

While developing, restarting the server on every code change is too time consuming. Nodemon  allows server live reload:

 npm install --save-dev nodemon
nodemon ./server/bin/www


Additional reference

node-postgres-sequelize  

code base for this post

Guy Y.
Software Developer
Back to Blog