Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Curt Gratz
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Curt Gratz

Validating / Consuming Mount-Path Parameters With Middleware In Express.js And Node.js

By
Published in Comments (2)

At InVision App, Inc, in one of our Node.js domain services, all calls to the API have to contain a "Team ID". Originally, we were passing this through to the API as part of a custom HTTP Header; but, we were just told that we have to start passing it through as a route-prefix instead. In order to not have to define the same prefix on every route, we're using an Express Router to mount the core functionality at the team-based prefix. But, this requires us to change the way we validate and consume the Team ID. As someone who's new to Express.js, it wasn't immediately clear how to do this; so, I wanted to look at a few different options.

In Express.js, you can mount a Router - or even an entire Express.js application - on a route prefix so that you don't have to define the route prefix over and over again inside the route internals:

app.use( "/v1/route/prefix", router_or_sub_app )

Now, your Express.js router (or sub-application) doesn't need to know anything about the "/v1/route/prefix" prefix; and, routes defined within the router will automatically be appended to the given prefix.

In the above case, the route prefix is static. But, it doesn't have to be - just like any other route, the mount-path can contain dynamic parameters:

app.use( "/teams/:teamID", router_of_sub_app )

Now, when Express.js is processing the request, it will extract the :teamID value from the matched mount-path and put it in the request.params collection. Accessing that request.params value, however, is not immediately clear. By default, mounted routers don't inherit the params from their parent context. This feature can be overridden; but, should doing that be considered a code smell?

To explore this idea, I came up with three different ways to validate and consume the route params in the mount-path. My first thought was to push the validation down into the mounted router:

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

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

var app = express();
var router = express.Router({
	// We're about to mount this router on a mount-path that contains a parameter. By
	// default, the mount-path parameter is hidden to the router. However, if we merge
	// the params from the parent context, the mount-path parameter will be visible to
	// the router's middleware where we can validate it.
	mergeParams: true
});

// Mount the router on a partial-URL that contains a named-parameter. This way, we don't
// have to include this path-prefix on every route that we define in our application.
app.use( "/teams/:teamID", router );

// Inside the Router, we can assume that every path contains the :teamID (from the
// parent context, thanks to the mergeParams options). As such, we can define global
// middleware in the router that validates the :teamID for every request.
router.use(
	function( request, response, next ) {

		if ( request.params.teamID !== "gold" ) {

			console.log( chalk.red.bold( "Team parameter rejected." ) );
			throw( new Error( "InvalidTeam" ) );

		}

		console.log( chalk.dim.italic( "Team parameter accepted." ) );
		next();

	}
);

// Define a route inside the router, mounted on the :teamID prefix.
router.get(
	"/users/:userID",
	function( request, response, next ) {

		console.log( chalk.dim( "User route matched." ) );
		response.send( "OK: " + request.params.userID );

	}
);

app.listen( "8080" );

In this approach, I'm using the Router option, "mergeParams", to tell Express.js to let the parent route params be visible within the context of the mounted router. This way, when I define global middleware as the first interceptor in the Router definition, I can access and validate the :teamID parameter.

Now, if we go to access a URL that contains an invalid team, like:

GET /teams/green/users/ben

... we get the following terminal output:

Consuming mount-path parameters in middleware in Express.js and Node.js.

As you can see, this worked perfectly well - the Router was able to access and validate the :teamID mount-path parameter from within its own middleware. But, there's something that feels a little funny about this. Not only does this require a non-default constructor option on the Router, it spreads the representational knowledge of the Team ID across two different contexts. It feels like we have a poor separation of concerns.

Ideally, I'd like the :teamID parameter validation to be closer to the :teamID definition. And, luckily, Express.js is flexible enough to allow for that. In Express.js, a mount-path can contain both a Router and middleware. So, in this next approach, I'm moving the consumption and validation middleware up one level to be part of the mount-path definition:

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

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

var app = express();
var router = express.Router();

// Mount the router on a partial-URL that contains a named-parameter. This way, we don't
// have to include this path-prefix on every route that we define in our application.
app.use(
	"/teams/:teamID",

	// This time, rather than pushing the :teamID validation down into the Router, we're
	// going to validate it using middleware directly on the mount-path. Now, no need to
	// merge params. We can start to treat the router as more prefix-agnostic.
	function( request, response, next ) {

		if ( request.params.teamID !== "gold" ) {

			console.log( chalk.red.bold( "Team parameter rejected." ) );
			throw( new Error( "InvalidTeam" ) );

		}

		console.log( chalk.dim.italic( "Team parameter accepted." ) );
		next();

	},

	// The router is still mounted on the :teamID prefix path.
	router
);

// Define a route inside the router, mounted on the :teamID prefix.
router.get(
	"/users/:userID",
	function( request, response, next ) {

		console.log( chalk.dim( "User route matched." ) );
		response.send( "OK: " + request.params.userID );

	}
);

app.listen( "8080" );

As you can see, we no longer need to use the "mergeParams" option when constructing our Router. Since the Router isn't managing the validation of Team ID, the Router doesn't need to know about parameters defined in the parent context. Now, only the mount-path context knows about the :teamID parameter and how to validate it. The mount-path control-flow will pass the request off to the validation middleware before the control-flow it passed down into the Router. As such, we keep the defintion and the consumption of the :teamID parameter in one place.

This feels like the cleaner solution since we have a better separation of concerns. But, I wanted to look at one more approach as a way to showcase the flexibility of Express.js as a web application framework. In this following approach, I'm going to use a sibling route to handle the validation:

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

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

var app = express();
var router = express.Router();

// This time, instead of putting the :teamID validation in the mount-path (or the mount-
// path router), we're going to put it in a sibling wild-card route. This route will
// match anything with the given prefix (thanks to the "*" pattern) and provide the
// :teamID validation before passing control onto the next matching route handler.
app.all(
	"/teams/:teamID/*",
	function( request, response, next ) {

		if ( request.params.teamID !== "gold" ) {

			console.log( chalk.red.bold( "Team parameter rejected." ) );
			throw( new Error( "InvalidTeam" ) );

		}

		console.log( chalk.dim.italic( "Team parameter accepted." ) );
		next();

	}
);

// Mount the router on a partial-URL that contains a named-parameter. This way, we don't
// have to include this path-prefix on every route that we define in our application.
// --
// NOTE: The biggest downside to this approach is that we have to define the route prefix
// twice - once in the .all() route above and once in the mount-path here.
app.use( "/teams/:teamID", router );

// Define a route inside the router, mounted on the :teamID prefix.
router.get(
	"/users/:userID",
	function( request, response, next ) {

		console.log( chalk.dim( "User route matched." ) );
		response.send( "OK: " + request.params.userID );

	}
);

app.listen( "8080" );

In this approach, I have two routes that are binding to the same prefix (sort of). The first route (which requires the "*" path component in order to do a wildcard match) performs the :teamID validation before it passes control-flow onto the next matching route (which is our mount-path).

This works, but it feels a little disjointed. Not only do we have to duplicate the concept of the :teamID parameter across two different routes, we also have to use a wild-card pattern in order to get the first route to work. There's nothing wrong with wild-card patterns; but, it just feels funny to be using the same partial-route match in two different places.

For me, the cleanest approach is the second one. By moving the :teamID mount-path parameter consumption and validation to the middleware defined in the mount-path route, it keeps all of the related aspects together in the same context. Not only do we avoid duplication of the path prefix, it also shields the Router from having to know anything about how the Team ID is being delivered to the application.

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

Reader Comments

1 Comments

The first example is obviously not the best but I like because most time, we found on Internet only "best practice" and not alternative way, so to learn, it's interesting to go through bad practice to know "why" it's not good practice.

15,902 Comments

@Astier,

Definitely - it's all a learning process. A lot of it is also just know which features even exist. Meaning, if you didn't know that Express offered an express.Router class, this wouldn't even be an option. So, as you learn more about the technology, you start to see more solutions to the same problems.

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