Manually Invoking Express.js Middleware Functions
The other day, I experimented with porting Framework One (FW/1) concepts into an Express.js web application. I love the class-based organization and inversion of control (IoC) that FW/1 offers; and, I wanted to see if something similar could be built on top of the middleware-based control flow that Express.js provides. In that experiment, I ended-up building all of the FW/1 life-cycle hooks as middleware functions because the whole next()-based callback mechanism felt very magical - like a blackbox. In order to peer into that blackbox, I wanted to further experiment with invoking Express.js middleware functions manually. This way, I may have more options in how I consume Express-compatible code going forward.
At its core, Express.js is just a stack of functions that gets called in series. In fact, it's easy to think about Express.js request handling like a Promise chain. Each middleware function in Express.js acts like a resolution handler (if it has 3 arguments) or a rejection handler (if it has 4 arguments). And, just like a Promise chain, each middleware function is capable of switching the request processing back and forth between a resolution and a rejection state (if you squint hard enough).
NOTE: Unlike a promise chain, Express.js middleware doesn't pass resolved values directly onto the next handler. Instead, Express.js middleware acts by augmenting the request and response objects.
So, if I want to invoke Express.js middleware functions manually, it makes sense to try and wrap their execution in a Promise chain, where each middleware function is proxied by an individual Promise callback. This way, the "next()" function that I pass into each middleware handler could act by moving the local Promise into a "resolved" or a "rejected" state. Which, by the way, is a utility function that basically all Node.js Promise libraries offer.
To experiment with this, I created a simple Express.js demo that has a single route handler. And, within that single route handler, I am manually invoking a series of middleware functions before rendering the final response. To do this, I am essentially reducing the collection of functions into a single Promise chain, which I then return to the calling context
// Require our core node modules. | |
var chalk = require( "chalk" ); | |
var express = require( "express" ); | |
var Q = require( "q" ); | |
var _ = require( "lodash" ); | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
// I complete in success, synchronously. | |
function middlewareOne( request, response, next ) { | |
console.log( chalk.red.bold( "[ONE]:" ), "running..." ); | |
next(); | |
} | |
// I completely in error, synchronously. | |
function middlewareTwo( request, response, next ) { | |
console.log( chalk.red.bold( "[TWO]:" ), "running..." ); | |
throw( new Error( "Two went boom!" ) ); | |
} | |
// I completely in error, asynchronously. | |
function middlewareThree( request, response, next ) { | |
console.log( chalk.red.bold( "[THREE]:" ), "running..." ); | |
setTimeout( | |
() => { | |
next( new Error( "Three went boom!" ) ); | |
}, | |
100 | |
); | |
} | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
var app = module.exports = express(); | |
// For this exploration, we only have one route to handle all requests. But, as an | |
// experiment, the route handler will explicitly invoke the middleware functions as a | |
// means to demystify how the middleware works. | |
// -- | |
// NOTE: I am NOT RECOMMENDING that you do this in your Express.js application - I am | |
// doing this because next() feels a bit too magical and I want to make it mundane. And, | |
// because I have reasons to want to be able to run it explicitly from within other | |
// middleware instances. | |
app.get( | |
"/", | |
function ( request, response, next ) { | |
console.log( chalk.green.bold( "Processing:" ), request.path ); | |
// Let's define the collection of middleware to invoke explicitly. | |
// -- | |
// NOTE: I am creating an arbitrary array structure to test the flattening. | |
var middlewares = [ | |
middlewareOne, | |
[ | |
middlewareTwo, | |
// Since we know that TWO will throw an error, we'll catch it with error | |
// handling middleware and turn it back into a "resolved" chain. | |
function catchTwo( request, response, error, next ) { | |
console.log( chalk.dim.italic( "Two threw an error:", error.message ) ); | |
next(); | |
}, | |
[ | |
middlewareThree, | |
// Since we know that THREE will throw an error, we'll catch it with | |
// error handling middleware and turn it back into a "resolved" chain. | |
function catchThree( request, response, error, next ) { | |
console.log( chalk.dim.italic( "Three threw an error:", error.message ) ); | |
next(); | |
} | |
] | |
] | |
]; | |
// Now, let's manually run the collection of middleware and then pipe the | |
// control flow back into the current request resolution. | |
invokeMiddleware( middlewares ) | |
.then( | |
() => { | |
console.log( chalk.red.bold( "[REQUEST]:" ), "closing..." ); | |
response.send( "Hello world" ); | |
} | |
) | |
.catch( next ) | |
; | |
} | |
); | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
// I invoke the given middlewares and return a promise. | |
// -- | |
// NOTE: Internally, Express.js DOES NOT USE PROMISES for the middleware; as such, | |
// using promises will add some performance overhead and a forced asynchronous control | |
// flow whereas the core middlware may run synchronously. | |
function invokeMiddleware( middlewares, request, response ) { | |
var promise = _ | |
// Express automatically collapses all of the middleware into a flat array. As | |
// such, we want to flatten the given collection before we reduce it. | |
.flattenDeep( [ middlewares ] ) | |
.reduce( | |
( chain, handler ) => { | |
// If the handler accepts four explicit arguments, it is an error handler; | |
// append it to the promise chain as a catch(). | |
if ( handler.length === 4 ) { | |
var tail = chain.catch( | |
( error ) => { | |
return( invokeHandler( handler, [ request, response, error ] ) ); | |
} | |
); | |
// Otherwise, if the handler accepts three explicit arguments, it is a | |
// normal handler; append it to the promise chain as a then(). | |
} else if ( handler.length === 3 ) { | |
var tail = chain.then( | |
() => { | |
return( invokeHandler( handler, [ request, response ] ) ); | |
} | |
); | |
// If the handler accepts an unexpected number of arguments, just bypass | |
// it, passing the existing chain onto the next handler. | |
} else { | |
var tail = chain; | |
} | |
return( tail ); | |
}, | |
Q.when() // Initial promise chain. | |
) | |
; | |
return( promise ); | |
// I wrap the given handler invocation in a promise. | |
function invokeHandler( handler, handlerArgs ) { | |
var deferred = Q.defer(); | |
// NOTE: We don't need to worry about synchronous errors during invocation | |
// because the handler is already being invoked inside of the Promise handler | |
// in the above reduction. | |
handler.call( null, ...handlerArgs, deferred.makeNodeResolver() ); | |
return( deferred.promise ); | |
} | |
} |
As you can see, the collection of middleware is composed of an arbitrarily nested array. I did this simply because Express.js allows it and I wanted to build that into my invocation strategy. This array is then flattened and reduced into a Promise chain. If the given middleware has three arguments, I treat it like normal middleware and fold it into the Promise chain as a .then() handler. And, if the given middleware has four arguments, I treat it as an error handler and fold it into the Promise chain as a .catch() handler.
If we bootup this Express.js application and make a request to it, we get the following terminal output:

As you can see, each of the middleware was successfully invoked - manually - from within my one Express.js route handler. And, each of the error handlers was able to successfully catch the thrown error and convert the Promise chain wrapper back to a resolved state so that our top-level router handler could successfully complete the request.
To be clear, I am not recommending that people start invoking Express.js middleware manually. Middleware makes sense as "middleware". I just wanted to experiment with this because I can think of some situations in which I may want to leverage existing middleware functions in a context where the middleware architecture has been abstracted away from direct use. And, if nothing else, I feel that experiments like this make Express.js and middleware seem like less of a blackbox, which is good for the old mental model.
Want to use code from this post? Check out the license.
Reader Comments
@All,
For what it's wroth, Scott Rippey on my team had a thought that this approach would be helpful for unit-testing middleware since you wouldn't need to spin up / mock-out an entire Express app just see if the inputs / outputs of middleware make sense.
@All,
CAUTION: I just realized that my *** ERROR *** object is in the wrong parameter location. It is supposed to be in the FIRST parameter, but I am passing it as the THIRD. In my demo, it happens to work because I am manually invoking the handler (so I basically messed it up on both sides of the call-stack).