Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at InVision Office 2011 (New York City) with: Lindsey Root and Adam Root and Clark Valberg
Ben Nadel at InVision Office 2011 (New York City) with: Lindsey Root@lowcarblindsey ) , Adam Root@adamroot ) , and Clark Valberg@clarkvalberg )

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

By Ben Nadel on

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.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

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

Reply to this Comment

@Š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.

Reply to this Comment

@Ben,

It's here http://xiaogliu.github.io/2017/04/13/%E5%9C%A8AngularJS%E4%BD%BF%E7%94%A8-scope%E8%BF%98%E6%98%AFscope/. Thanks a lot!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.