Jialun Liu
Jialun Liu

Reputation: 331

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"....

Upvotes: 11

Views: 15290

Answers (4)

Rémi Becheras
Rémi Becheras

Reputation: 15222

connect-sequence: a dedicated node module for that specific purpose:

You can just use the module connect-sequence which is designed for that purpose:

npm install --save connect-sequence

see:

and then, here an example of usage:

/**
 * Product API
 * @module
 */

var ConnectSequence = require('connect-sequence')
var productsController = require('./products.controller')

module.exports = productRouter

function productRouter (app) {
  app.route('/api/products/:productId')
  .get(function (req, res, next) {
    // Create a ConnectSequence instance and setup it with the current `req`,
    // `res` objects and the `next` callback
    var seq = new ConnectSequence(req, res, next)

    // build the desired middlewares sequence thanks to:
    // - ConnectSequence#append(mid0, ..., mid1),
    // - ConnectSequence#appendList([mid0, ..., mid1])
    // - and ConnectSequence#appendIf(condition, mid)

    if (req.query.filter) {
      seq.append(productsController.filter)
    }

    if (req.query.format) {
      seq.append(
        productsController.validateFormat,
        productsController.beforeFormat,
        productsController.format,
        productsController.afterFormat
      )
    }

    // append the productsController.prepareResponse middleware to the sequence
    // only if the condition `req.query.format && req.formatedProduct` is true
    // at the moment where the middleware would be called.
    // So the condition is tested after the previous middleware is called and thus
    // if the previous modifies the `req` object, we can test it.
    seq.appendIf(isProductFormatted, productsController.prepareResponse)

    seq.append(productsController.sendResponse)

    // run the sequence
    seq.run()
  })

  app.param('productId', function (req, res, next, id) {
    // ... yield the product by ID and bind it to the req object
  })

  function isProductFormatted (req) {
    return Boolean(req.formatedProduct)
  }
}

This is open source, PR are welcome!

If you like and use connect-sequence, but if you find bug or need some new features, feel free to post issues or submit pull requests!

Upvotes: 6

Timbo925
Timbo925

Reputation: 281

Based on the idea behind @Ryan's code I came up with this function. It executes a list of middleware in order binding the variables as needed, allowing everything to be executed by just executeMiddlewareList([middleware1, middleware2...], req, res, next);. For each middlewarereq, res is passed and the callback from async.eachSeries. This means when next() is called inside the middleware, then next one will be handled from the list. If middleware throws an error with next(err), execution will stop and you can manually handle this.

function executeMiddlewareList (middlewareList, req, res, next) {
   async.eachSeries(middlewareList, function(middleware,callback) {
      middleware.bind(null,req,res,callback)()
   }, function(err) {
      if (err) return res.status(500).json({error: err});
      next();
   })
}

function testMid (number) {
   return function (req, res, next) {
      log.debug('req.test from', req.test, " to ", number);
      req.test=number;
      next();
   }
}

router.get('/test', function(req, res, next) {
   m.executeMiddlewareList([test(1), test(2)], req, res, next);
   //Output: req.test from undefined to 1
   //        req.test from 1 to 2
}, function (req,res) {
    //Do stuff after the executeMiddlewareList, req.test = 2
})

Upvotes: 2

Ryan
Ryan

Reputation: 5973

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

Upvotes: 15

Jialun Liu
Jialun Liu

Reputation: 331

Finally, I find the answer according the Ryan's, the code would look like this:

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;
        }

        console.log(fn[0]);
        console.log(middleware);

        operations.push(middleware.bind(null, req, res)); // 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);
        }
        console.log('middleware get executed');
        // no errors so pass control back to express
        next();
    });

}

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

The key step is indeed the operations.push(middleware.bind(null, req, res)); Which to be honest, I don't understand what does it mean exactly. I know this is passing "req" and "res" variable into the middleware, but I don't get what the point of the "null" in the front. Much appreciated if someone could help me clarify this.

Upvotes: 0

Related Questions