Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Jamie Martin
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Jamie Martin

Express.js Middleware Can Be Arbitrarily Nested Within A Route In Node.js

By
Published in Comments (6)

As someone who is just a couple of days into learning Express.js for Node.js-based web applications, I'm still trying to build a solid mental model for the core mechanics of the framework. And, while I have a thin understanding of what middleware is, one thing that was unclear to me was how collections of middleware related to route execution. So, I did a little experimenting; and, it seems that middleware can be arbitrarily nested within a route.

To try this out, I just created a single route for .get("/') and then started trying all different kinds of middleware combinations. I even tried instantiating a secondary Router and using that as middleware in the midst of all the other middleware being applied to the root route:

// Require our core node modules.
var chalk = require( "chalk" );
var express = require( "express" );

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

var app = module.exports = express();

// For this exploration, we only have one route. But, that route has many middlewares
// that are arbitrarily nested.
app.get(
	"/",
	function base( request, response, next ) {

		console.log( chalk.red.bold( "Handling '/' route" ) );
		next();

	},
	function a1( request, response, next ) {

		console.log( chalk.dim.bold( "Middleware:" ), "a1" );
		next();

	},
	[
		function a2_1( request, response, next ) {

			console.log( chalk.dim.bold( "Middleware:" ), "a2_1" );
			next();

		},
		[
			function a2_2_1( request, response, next ) {

				console.log( chalk.dim.bold( "Middleware:" ), "a2_2_1" );
				next();

			},
			function a2_2_2( request, response, next ) {

				console.log( chalk.dim.bold( "Middleware:" ), "a2_2_2" );
				next();

			}
		]
	],
	[
		function a3_1( request, response, next ) {

			console.log( chalk.dim.bold( "Middleware:" ), "a3_1" );
			next();

		},
		function a3_2( request, response, next ) {

			console.log( chalk.dim.bold( "Middleware:" ), "a3_2" );
			next();

		}
	],

	// Try nesting a Router as middleware (with internally nested middleware).
	express.Router()
		.use(
			function a4_r1( request, response, next ) {

				console.log( chalk.dim.bold( "Middleware:" ), "a4_r1" );
				next();

			},
			[
				function a4_r2_1( request, response, next ) {

					console.log( chalk.dim.bold( "Middleware:" ), "a4_r2_1" );
					next();

				},
				function a4_r2_2( request, response, next ) {

					console.log( chalk.dim.bold( "Middleware:" ), "a4_r2_2" );
					next();

				}
			]
		)
		.use(
			function a4_r3( request, response, next ) {

				console.log( chalk.dim.bold( "Middleware:" ), "a4_r3" );
				next();

			}
		)
	,

	function a5( request, response, next ) {

		console.log( chalk.dim.bold( "Middleware:" ), "a5" );
		next();

	},

	// Setup the final handler - something needs to actually end the request.
	function end( request, response, next ) {

		console.log( chalk.dim.bold( "Middleware:" ), "done" );
		response.send( "Done." );

	},

	// Setup the error handler.
	// --
	// NOTE: We don't actually need it for this demo since nothing it going to throw
	// an error; but, I think it's nice to see that each route / mount can have its
	// own error handler if needed.
	function handleError( error, request, response, next ) {

		console.log( error );
		response.send( "Error." );

	}
);

As you can see, not only am I passing N arguments to the .get() route method, the arguments are a combination of top-level and nested middleware functions. One is even a Router instance that, itself, has nested middleware arguments. And, when we spin this Express.js server up and make a browser request, we get the following terminal output:

Express.js flattens middleware collections before storing them internally.

As you can see, all of our middleware were invoked, regardless of how they were organized in the route definition. If you look at the Express.js code, you can see that, internally, when you setup a route, the middleware is being flattened before it is being applied to the internal application definition:

var callbacks = flatten( slice.call( arguments, offset ) );

This means that Express.js turns our mixture of arbitrarily-nested middleware into a flat collection of middleware that Express can just invoke - in-series - as it evaluates the route handling. So, essentially, this kind of a collection:

[ a, [ b, [ c, d, e ], f, [ g ], h ]

... gets transformed into this kind of a collection:

[ a, b, c, d, e, f, g, h ]

... before it gets stored internally.

Another fun thing you might notice in my experiment is that the route has error-handling middleware. Since an error handler is just a middleware with a special invocation signature (4 arguments exactly), each route can have its own error handler if it wanted to. This is kind of like a Promise chain where each .then() can have its own "catch" handler; or, it can choose to defer to a catch-handler at the bottom of the Promise chain. Or, it can even use both, if it chooses to propagate the error.

For those of you with Express.js experience, this should be nothing new. But, as someone who is rather new to Express.js, it wasn't exactly clear how middlware could be organized within a route. Now that I see that middleware collections get flattened before they are stored, it makes the whole request / response control flow much more clear.

Want to use code from this post? Check out the license.

Reader Comments

16 Comments

One cool feature that I've recently started using is having conditional middleware that only gets executed inside its router for specific paths, e.g.

app.use('/ajax', ajax)

where ajax is a Router in a separate module, which can then .use() middleware just like app, but that middleware only runs for requests whose paths start with /ajax.

So, when a requests is processed, if its path doesn't start with /ajax, the entire ajax router is skipped over and none of its middleware runs.

15,883 Comments

@Šime,

That is pretty cool. It looks like the starter app generated by "express generate" does something a bit similar. I don't have it front of me, but I think it has something like this in the app.js:

app.use( "/index", require( "./routes/index" ) );
app.use( "/users", require( "./routes/users" ) );

... which I believe was using an express.Router() under the hood.

Right now, I'm having some fun with trying to create a request flow on top of the Express middleware to match another framework that I am used to using in ColdFusion. More than anything, it's just fun to see how all the middlewares inter-relate.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel