Jialun Liu Jialun Liu - 5 months ago 23
Node.js Question

How to make dynamic chain of middleware in express.js

I am currently working on a project to develop an API manager to control an existing API.

It contains a list of "before" and "after" middlewares, which are used to do things like security checking and logging. And a "service" middleware to do http request to the existing API. But the problem is that I want to make the order the middleware being executed to be dynamic, meaning that I could load some configuration file to change the order the middleaware get executed every time the request comes in.

here is my previous code:

'use strict';
// Loading the express library
var express = require('express');
var app = express();

var service = require('./routes/index');


// Testing configurable middleware
var confirguration = {
before1: {
priority: 100,
enable: true
},
before2: {
priority: 80,
enable: true
},
service: {
priority: 50,
enable: true
},
after1: {
priority: 30,
enable: true
},
after2: {
priority: 10,
enable: true
}
}

var before1 = require('./example_middleware/before1');
var before2 = require('./example_middleware/before2');
var after1 = require('./example_middleware/after1');
var after2 = require('./example_middleware/after2');
// Fake request to simulate the /service
var fakeRequest = require('./example_middleware/fake_request');

// Function to sort the order of the middleware to be executed
var sortConfig = function(confirguration){
var sortable = [];
for (var middleware in confirguration)
// To make middlewares configurable
if (confirguration[middleware]['enable'] == true){
sortable.push([middleware, confirguration[middleware]['priority']]);
}

sortable.sort(function(a, b) {return b[1] - a[1]});
return sortable;
}

// var sortedConfig = [];
var middlewareSet = new Array();
app.use('/test', function(request, response, next){
var middleware;
var sortedConfig = sortConfig(confirguration);

for (var i in sortedConfig){
switch(sortedConfig[i][0]){
case 'before1':
middleware = before1;
break;
case 'before2':
middleware = before2;
break;
case 'service':
middleware = fakeRequest;
break;
case 'after1':
middleware = after1;
break;
case 'after2':
middleware = after2;
break;
}


// console.log(sortedConfig[i][0]);
// Execute the middleware in expected order
middlewareSet.push(middleware);
}
// request.sortedConfig = sortedConfig;
console.log(middlewareSet);
console.log('middleware list sorted');
next();
});

app.use('/test', middlewareSet);


But I keep getting the same error message coming from the app.use() at the last line:


app.use() requires middleware functions


It works if I use:

app.use('/test', [before1, before2, fakeRequest, after1, after2]);


But it's not dynamic though, what did I misunderstand? There must be a way to do this in express.js.

Thanks in advance.

EDIT:
I modified my code according to Ryan's answer, here is the code:

var async = require('async');
app.use('/test', configurableMiddleWare);

function configurableMiddleWare(req, res, next) {

var operations = [];

var middleware;

var sortedConfig = sortConfig(confirguration);

// push each middleware you want to run
sortedConfig.forEach(function(fn) {

switch(fn[0]){
case 'before1':
middleware = before1;
break;
case 'before2':
middleware = before2;
break;
case 'service':
middleware = fakeRequest;
break;
case 'after1':
middleware = after1;
break;
case 'after2':
middleware = after2;
break;
}

operations.push(middleware); // could use fn.bind(null, req, res) to pass in vars
});

console.log('middleware list sorted');
// now actually invoke the middleware in series
async.series(operations, function(err) {
if(err) {
// one of the functions passed back an error so handle it here
return next(err);
}
// no errors so pass control back to express
next();
});

}


Just to make sure I haven't made any mistakes in my test middleware, here is an example of one of them:

'use strict';

var express = require('express');
var router = express.Router();

router.route('/')
.all(function(request, response, next){
console.log('This is middleware BEFORE1');
next();
});


module.exports = router;


Now, when I run my application, I got the following error from npm:


TypeError: Cannot call method 'indexOf' of undefined


at Function.proto.handle (/Users/jialunliu/Documents/SOA_project/FAT-LADY/node_modules/express/lib/router/index.js:130:28)
at router (/Users/jialunliu/Documents/SOA_project/FAT-LADY/node_modules/express/lib/router/index.js:35:12)
at /Users/jialunliu/Documents/SOA_project/FAT-LADY/node_modules/async/lib/async.js:610:21
at /Users/jialunliu/Documents/SOA_project/FAT-LADY/node_modules/async/lib/async.js:249:17
at iterate (/Users/jialunliu/Documents/SOA_project/FAT-LADY/node_modules/async/lib/async.js:149:13)
at async.eachSeries (/Users/jialunliu/Documents/SOA_project/FAT-LADY/node_modules/async/lib/async.js:165:9)
at _asyncMap (/Users/jialunliu/Documents/SOA_project/FAT-LADY/node_modules/async/lib/async.js:248:13)
at Object.mapSeries (/Users/jialunliu/Documents/SOA_project/FAT-LADY/node_modules/async/lib/async.js:231:23)
at Object.async.series (/Users/jialunliu/Documents/SOA_project/FAT-LADY/node_modules/async/lib/async.js:608:19)
at configurableMiddleWare (/Users/jialunliu/Documents/SOA_project/FAT-LADY/app.js:135:11)



Which is coming from the line
async.series(operations, function(err){})


I am keep getting this kind of error message, saying the function could not read from this array of functions "operations"....

Answer

I think you are on the right track, you will just need to tweak a few things. I would register one top level function with app.use() and then do all of your dynamic stuff within that function. Updating my answer to a working example. Be sure to install async first npm install --save async

// define all middleware functions
var middleware = {
    mw1: function(req, res, next) {
        console.log('mw 1');
        next();
    },
    mw2: function(req, res, next) {
        console.log('mw 2');
        next();
    },
    mw3: function(req, res, next) {
        console.log('mw 3');
        next();
    },
    mw4: function(req, res, next) {
        console.log('mw 4');
        next();
    }

};

// register our "top level function"
app.use(configurableMiddleware);
var requestCount = 1; // this is just for the working example

function configurableMiddleware(req, res, next) {
    var isEvenRequest = requestCount++ % 2 === 0; // simple logic to alternate which "configurable" middleware to use

    var operations; // in the real world you could build this array dynamically, for now we just hardcode two scenarios as an example

    // Each request to http://localhost:3000 will alternate which middleware is used, so you will see a different log each time
    if(isEvenRequest) {
        console.log('Even request should log mw2 and mw4');
        // .bind(null, req, res) makes sure that the middleware gets the request and response objects when they are invoked, 
        // as of this point they still haven't been invoked...
        operations = [middleware.mw2.bind(null, req, res), middleware.mw4.bind(null, req, res)];
    }
    else {
        console.log('Odd request should log mw1 and mw3');
        operations = [middleware.mw1.bind(null, req, res), middleware.mw3.bind(null, req, res)];
    }

    // invoke each middleware in series - you could also do async.parallel if the order of middleware doesn't matter
    // using the async module: https://github.com/caolan/async
    async.series(operations, function(err) {
        if(err) {
            console.log('There was a problem running the middleware!');
            return next(err);
        }
        // all middleware has been run
        next();
    });
}

For more info on .bind() see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

Comments