4

I'm looking for the easiest & performant way to make a multitenant express.js app for managing projects.

Reading several blogs and articles, I figured out that, for my application, would be nice to have a database per tenant architecture.

My first try has been to use subdomains to detect the tenant, and then map the subdomain to a mongodb database.

I came up with this express middlewares

var mongoose = require('mongoose');
var debug = require('debug')('app:middleware:mongooseInstance');
var conns [];
function mongooseInstance (req, res, next) {
    var sub = req.sub = req.subdomains[0] || 'app';
    // if the connection is cached on the array, reuse it
    if (conns[sub]) {
        debug('reusing connection', sub, '...');
        req.db = conns[sub];
    } else {
        debug('creating new connection to', sub, '...');
        conns[sub] = mongoose.createConnection('mongodb://localhost:27017/' + sub);
        req.db = conns[sub];
    }
    next();
}
module.exports = mongooseInstance;

Then I register the models inside another middleware:

var fs = require('fs');
var debug = require('debug')('app:middleware:registerModels');
module.exports = registerModels;

var models = [];
var path = __dirname + '/../schemas';

function registerModels (req, res, next) {
    if(models[req.sub]) {
        debug('reusing models');
        req.m = models[req.sub];
    } else {
        var instanceModels = [];
        var schemas = fs.readdirSync(path);
        debug('registering models');
        schemas.forEach(function(schema) {
            var model = schema.split('.').shift();
            instanceModels[model] = req.db.model(model, require([path, schema].join('/')));
        });
        models[req.sub] = instanceModels;
        req.m = models[req.sub];
    }
    next();
}

Then I can proceed normally as any other express.js app:

var express = require('express');
var app = express();
var mongooseInstance = require('./lib/middleware/mongooseInstance');
var registerModels = require('./lib/middleware/registerModels');

app.use(mongooseInstance);
app.use(registerModels);

app.get('/', function(req, res, next) {
    req.m.Project.find({},function(err, pets) {
        if(err) {
            next(err);
        }
        res.json({ count: pets.length, data: pets });
    });
});

app.get('/create', function (req, res) {
    var p = new req.m.Project({ name: 'Collin', description: 'Sad' });
    p.save(function(err, pet) {
        res.json(pet);
    });
});

app.listen(8000);

The app is working fine, I don't have more than this right now, and I'd like to get some feedback before I go on, so my questions would be:

Is this approach is efficient? Take into account that a lot will be happening here, multiple tenants, several users each, I plan to setup webhooks in order to trigger actions on each instance, emails, etc...

Are there any bottlenecks/pitfalls I'm missing? I'm trying to make this scalable from the start.

What about the model registering? I didn't found any other way to accomplish this.

Thanks!

3
  • I'm about to do the exact same thing, i don't want to re invent the wheel but i couldn't find a proper answer to this approach. Or maybe a good tutorial on how to, or maybe, what to be aware of this approach. Commented Apr 13, 2015 at 23:40
  • The only thing i can add is that adding the models on a middleware is a little unperformant, you re adding them on everty request. I would add them when starting the app (if the database already exists) Commented Apr 13, 2015 at 23:56
  • @narc88 I agree with you on that, but how do you know how to select the database in the first place? That's why I tried caching the models inside the array, so the actual model registering happens only in the first request, if I'm not missing something. Commented Apr 14, 2015 at 1:03

1 Answer 1

7

Is this approach is efficient? Are there any bottlenecks/pitfalls I'm missing?

This all seems generally correct to me

What about the model registering?

I agree with @narc88 that you don't need to register models in middleware.

For lack of a better term, I would use a factory pattern. This "factory function" would take in your sub-domain, or however you decide to detect tenants, and return a Models object. If a given middleware wants to use its available Models you just do

var Models = require(/* path to your Model factory */);

...

// later on inside a route, or wherever
var models = Models(req.sub/* or req.tenant ?? */);
models.Project.find(...);

For an example "factory", excuse the copy/paste

var mongoose = require('mongoose');
var fs = require('fs');
var debug = require('debug')('app:middleware:registerModels');

var models = [];
var conns = [];
var path = __dirname + '/../schemas';

function factory(tenant) {
    // if the connection is cached on the array, reuse it
    if (conns[tenant]) {
        debug('reusing connection', tenant, '...');
    } else {
        debug('creating new connection to', tenant, '...');
        conns[tenant] = mongoose.createConnection('mongodb://localhost:27017/' + tenant);
    }

    if(models[tenant]) {
        debug('reusing models');
    } else {
        var instanceModels = [];
        var schemas = fs.readdirSync(path);
        debug('registering models');
        schemas.forEach(function(schema) {
            var model = schema.split('.').shift();
            instanceModels[model] = conns[tenant].model(model, require([path, schema].join('/')));
        });
        models[tenant] = instanceModels;
    }
    return models[tenant];
}

module.exports = factory;

Aside from potential (albeit probably small) performance gain, I think it also has the advantage of:

  • doesn't clutter up the request object as much
  • you don't have to worry as much about middleware ordering
  • allows more easily abstracting permissions for a given set of models, i.e. the models aren't sitting on the request for all middleware to see
  • This approach doesn't tie your models to http requests, so you might have flexibility to use the same factory in a job queue, or whatever.
Sign up to request clarification or add additional context in comments.

5 Comments

@jwags it's a good apprach, considering your example what about this: // schema file ProjectSchema = .... ... module.exports = function(database) { var conn = mongoose.createConnection(url + database); return conn.model('Project', ProjectSchema); };` That way I only get the model I'm interested on a particular request.
@narc88 I haven't run the exact code above, but I have have used something similar is several projects.
@ricardocasares That sounds like a fine approach. My answer is a bit long, but all I was really trying to say is that I wouldn't attach my Models to the request object inside middleware
Have someone tried it?. I'll try this approach this week on a project i'm working on.
@jwags I've used it, i had to modify a couple of lines, but it works. i've exposed models as schemas not much more. Great answer! thumbs up!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.