Dynamic routes with Express.js -- is this even possible?

mongodb, node.js

Solution

I would suggest two changes:

- Move this menu attachment thing to a separate module.

- While you're at it, do some caching.

Proof of concept menu db function, made async with setTimeout, you'll replace it with actuall db calls.

// menuitems is cached here in this module. You can make an initial load from db instead.
var menuitems = [];
// getting them is simple, always just get the current array. We'll use that.
var getMenuItems = function() {
    return menuitems;
}

// this executes when we have already inserted - calls the callback
var addMenuItemHandler = function(newItem, callback) {
    // validate that it's not empty or that it does not match any of the existing ones
    menuitems.push(newItem);
    // remember, push item to local array only after it's added to db without errors
    callback();
}
// this one accepts a request to add a new menuitem
var addMenuItem = function(req, res) {
    var newItem = req.query.newitem;

    // it will do db insert, or setTimeout in my case
    setTimeout(function(newItem){
        // we also close our request in a callback
        addMenuItemHandler(newItem, function(){
            res.end('Added.');
        });

    }, 2000);
};

module.exports = {
    addMenuItem: addMenuItem,
    getMenuItems: getMenuItems
}

So now you have a module menuhandler.js. Let's construct it and use it in our app.

var menuHandler = require('./menuhandler');
var app = express();
// config, insert middleware etc here

// first, capture your static routes - the ones before the dynamic ones.
app.get('/addmenuitem', menuHandler.addMenuItem);
app.get('/someotherstaticroute', function(req, res) {
    var menu = menuHandler.getMenuItems();
    res.render('someview', {menu: menu});
});


// now capture everything in your menus.
app.get('/:routename', function(req, res){
    // get current items and check if requested route is in there.

    var menuitems = menuHandler.getMenuItems();
    if(menuitems.indexOf(req.params.routename) !== -1) {
        res.render('myview', {menu: menuitems});
    } else {
        // if we missed the route, render some default page or whatever.
    }
});

app.get('/', function(req, res) {
    // ...
});

Now you don't go to db if there were no new updates (since menuitems array is always up to date) so your initial view is rendered faster (for that 1 db call, anyway).

Edit: oh, I just now saw your Model.js. The problem there is that `this` refers to the object you have returned:

{
    setDB: function(db) {
        this.db = db;
    },
    getlist: function(callback, query) {
        this.db.find(query || {}, function (err, doc) { callback(doc) });
    }
}

So, no db by default. And since you attach something to the `app` in the initial pageload, you do get something.

But in your current update function, you attach stuff to the new app (`reqApp = req.app`), so now you're not talking to the original app, but another instance of it. And I think that your subsequent requests (after the update) get the scope all mixed up so lose the touch with the actual latest data.

Problem

Every time I update the database with a new menu item, I'm trying to get the routing to update with one more route. Here's my sad little ugly attempt: Here in app.js, I check the menu database and shazaam...routes are made on the fly at startup. Cool!: ``` // in app.js // var attachDB = function(req, res, next) { req.contentdb = db.content; req.menudb = db.menu; req.app = app; // this is the express() app itself req.page = PageController; next(); }; db.menu.find({}, function (err, menuitems){ for(var i=0; record = menuitems[i]; i++) { var menuitem = record.menuitem; app.all('/' + menuitem, attachDB, function(req, res, next) { console.log('req from app all route: ',req) PageController.run(menuitem, req, res, next); }); } http.createServer(app).listen(config.port, function() { console.log( '\nExpress server listening on port ' + config.port ); }); }); ``` Not real elegant but it's a proof of concept. Now here's the problem: When I save a new menu item in my Admin.js file, the database get's updated, the router seems to get updated but something about the request just blows up after clicking on a menu link with a dynamically created route Many things in the request seem to be missing and I feel like there is something fundamental I don't understand about routing, callbacks or perhaps this is just the wrong solution. Here's what the function responsible for creating a new menu item and creating a new route in my Admin.js file looks like: ``` // in Admin.js // menuItem: function(req, res, callback) { var returnMenuForm = function() { res.render('admin-menuitem', {}, function(err, html) { callback(html); }); }; var reqMenudb = req.menudb, reqContentdb = req.contentdb, reqApp = req.app, reqPage = req.page; if(req.body && req.body.menuitemsubmitted && req.body.menuitemsubmitted === 'yes') { var data = { menuitem: req.body.menuitem }; menuModel.insert( data, function(err) { if (err) { console.log('Whoa there...',err.message); returnMenuForm(); } else { // data is inserted....great. PROBLEM...the routes have not been updated!!! Attempt that mimics what I do in app.js here... reqApp.all('/' + data.menuitem, function(req, res, next) { // the 2 db references below are set with the right values here req.contentdb = reqContentdb; req.menudb = reqMenudb; next(); }, function(req, res, next) { reqPage.run(data.menuitem, req, res, next); }); returnMenuForm(); } }); } else { returnMenuForm(); } }, ``` Saving the data in the admin section works fine. If you console log app.routes, it even shows a new route which is pretty cool. However after refreshing the page and clicking the link where the new route should be working, I get an undefined error. The admin passes data to my Page controller: ``` // in PageController.js // module.exports = BaseController.extend({ name: "Page", content: null, run: function(type, req, res, next) { model.setDB(req.contentdb); /* <-- problem here, req.contentdb is undefined which causes me problems when talking to the Page model */ var self = this; this.getContent(type, function() { var v = new View(res, 'inner'); self.navMenu(req, res, function(navMenuMarkup){ self.content.menunav = navMenuMarkup; v.render(self.content); }); }); }, getContent: function(type, callback) { var self = this; this.content = {} model.getlist(function(records) { if(records.length > 0) { self.content = records[0]; } callback(); }, { type: type }); } ``` Lastly, the point of error is here in the model ``` // in Model.js // module.exports = function() { return { setDB: function(db) { this.db = db; }, getlist: function(callback, query) { this.db.find(query || {}, function (err, doc) { callback(doc) }); }, ``` And here at last, the 'this' in the getlist method above is undefined and causes the page to bomb out. If I restart the server, everything works again due to my dynamic loader in app.js. But isn't there some way to reload the routes after a database is updated?? My technique here does not work and it's ugly to be passing the main app over to a controller as I'm doing here.

Original source