Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: David Phipps
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: David Phipps ( @phipps73 )

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

By Ben Nadel on

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.



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

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.

Reply to this Comment

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

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.