Express Initializer

2017-03-21

Preface

A typical Express app has infrastructure dependencies, such as database, redis, etc. All these dependencies should be ready at the very beginning, so we get a checklist.

A good beginning is half the battle. In this article, we will focus on initialization steps.

Globals? Yes!

Global variables are usually forbidden in code style guideline. The top one reason is global variables can be easily modified by mistake, and this mistake is hard to track.

Fortunately, we have Object.defineProperty in JavaScript. Using it, we can define a property on the global object, and not allow the property being modified.

function addGlobalConst(name, value) {
Object.defineProperty(global, name, {value: value})
}

addGlobalConst('addGlobalConst', addGlobalConst)
addGlobalConst('_', require('lodash'))
addGlobalConst('Promise', require('bluebird'))
addGlobalConst('fs', Promise.promisifyAll(require('fs')))

We define a function addGlobalConst to shorten the code. First, we add addGlobalConst itself to global object. After that we can use addGlobalConst anywhere.

Lodash is my favorite utils module, I let it to occupy _ symbol.

Bluebird is my favorite promise module, I’d like to give the name Promise to bluebird. After that, we can use Promise.promisifyAll to transfer all the APIs of fs module from callback nested style to promise chain style.

addGlobalConst('Sequelize', require('sequelize'))
addGlobalConst('conf', require('config'))
addGlobalConst('axios', require('axios'))
addGlobalConst('backbone', new (require('events').EventEmitter)())
addGlobalConst('box', require('../box'))

We can add more references to global object, as listed above. Sequelize is for database. conf is for config file, we can access config items by conf.item. axios is a powerful HTTP client. These are modules from npm.

backbone is just an EventEmitter, we can use it handle event globally. Why use this name? Because I am familiar with this name. In 2013, Backbone was very famous in front-end world. Events are used heavily in backbone. If you want to subscribe an event, just write down:

Backbone.on('eventName', function(data){
})

As the name, backbone is like the event bus of app. If you want to trigger a global event, just send it to event bus.

Every project has a utils file, which is tiny in the beginning, but soon grows into a huge pile of code. In my project, I use the name box, because it’s short and represents the right meaning.

When it’s hard to determine where to put a tool function, just put it into box. Keep the handy toolbox near you, if in need, just type box.toolName, I am sure you will love it.

Database Ready

A big system may connect more than one databases. Each database goes with a connection block in the config file, and a Sequelize instance in source file.

In config file:

db: {
db1: {
database: 'db1',
user: 'user1',
password: 'password1',
options: {
host: '127.0.0.1',
}
},
db2: {
database: 'db2',
user: 'user2',
password: 'password2',
options: {
host: '127.0.0.1',
}
},
}

In project root, we create a db directory, and for each database we create a sub-directory with the same name as the database. For example, we have two databases, db1 and db2:

We put all models of db2 into db2 directory, for example Model is one of the models of db2, Model.js is like this:

module.exports = function (sequelize, DataTypes) {
return sequelize.define('Model',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
text: {
type: DataTypes.STRING
}
},
{
debug: true
})
}

Now, all the databases are located in db directory, all model files are located in their database sub-directory. It’s time to do some automations: import models into database(sequelize instance), and put the database under the global db object.

/db/index.js

const db = {}

function importModels(database, relativeDirPath) {
db[relativeDirPath] = database

let fullDirPath = `${__dirname}/${relativeDirPath}`

fs.readdirSync(fullDirPath).filter(function (file) {
return file.indexOf('.') !== 0
}).forEach(function (file) {
let model = database.import(`${fullDirPath}/${file}`)
database[model.name] = model

Object.keys(database).forEach(function (modelName) {
if ('associate' in database[modelName]) {
database[modelName].associate(database)
}
})
})
}

fs.readdirSync(__dirname)
.forEach(entry => {
let dirPath = `${__dirname}/${entry}`
let stat = fs.statSync(dirPath)
if (stat.isDirectory() && entry.indexOf('.') !== 0) {
let dbName = entry
let dbConf = conf.db[dbName]
let db = new Sequelize(dbConf.database, dbConf.user, dbConf.password, dbConf.options)
importModels(db, dbName)
}
})

module.exports = db

The importing work are completely synchronous, so once you have required the module, you get all the models ready to use.

Sequelize has a sync function which can create tables according to our models automatically. It’s very convenient, especially in development environment.

/initializer/init-db.js

addGlobalConst('db', require('../db'))

module.exports = () => {
return Promise.each(_.values(db), db => {
return db.sync()
.then(() => {
console.log(`db sync success: ${db.config.database}`)
})
.catch(err => {
console.error(`db sync fail: ${db.config.database};`, err)
throw err
})
})
.then(() => {
console.log('init db success')
})
}

Redis Ready

If you don’t need redis at all, just skip this part.

Redis is so important that we need make sure it’s ready to use at the beginning.

The redis npm package has two problems should be resolved. First, change the APIs from callback style to promise style. Second, handle the error event of redis client, otherwise it will terminate the process.

/initializer/init-redis.js

addGlobalConst('redis', require('redis'))
Promise.promisifyAll(redis.RedisClient.prototype)
Promise.promisifyAll(redis.Multi.prototype)

module.exports = (mandatory = false) => {
const redisClient = redis.createClient(conf.redis)
addGlobalConst('redisClient', redisClient)

return new Promise((resolve, reject) => {
function redisError(err) {
console.trace('here')
console.error('redis error', err)
}

function connectFail(err) {
console.error('init redis fail', err)
redisClient.on('error', redisError)
if(mandatory){
reject(err)
}
else{
resolve()
}
}

function connectSuccess() {
console.log('init redis success')
redisClient
.removeListener('error', connectFail)
.on('error', redisError)
resolve()
}

redisClient
.once('ready', connectSuccess)
.once('error', connectFail)
})
}

If you don’t wanna redis block the app starting, leave the mandatory argument with the default value false . If redis is critical for your app, specify this argument to true.

The Initializer

Now it’s time to put all things together. We’d better leave only one entry to do all the initialization work. This entry’s code is as below: /initializer/index.js

function addGlobalConst(name, value) {
Object.defineProperty(global, name, {value: value})
}

addGlobalConst('addGlobalConst', addGlobalConst)
addGlobalConst('_', require('lodash'))
addGlobalConst('Promise', require('bluebird'))
addGlobalConst('fs', Promise.promisifyAll(require('fs')))
addGlobalConst('Sequelize', require('sequelize'))
addGlobalConst('conf', require('config'))
addGlobalConst('axios', require('axios'))
addGlobalConst('backbone', new (require('events').EventEmitter)())
addGlobalConst('box', require('../box'))

const initDb = require('./init-db')
const initRedis = require('./init-redis')

exports.init = () => {
return initDb()
.then(() => {
return initRedis()
})
}

After finishing the entire initialization, then we start the HTTP server. If something goes wrong, we have to exit the process. So let’s make some changes to /bin/www:

const initializer = require('../initializer')

initializer.init()
.then(() => {
console.log('init all success!')
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
})
.catch(err => {
console.error('init fail', err)
process.exit(1)
})

Summary

The whole project source code is on GitHub.

回主页
京ICP备14007233-1号