Skip to content

Instantly share code, notes, and snippets.

@zcaceres
Last active April 4, 2024 09:44
Show Gist options
  • Star 117 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save zcaceres/f38b208a492e4dcd45f487638eff716c to your computer and use it in GitHub Desktop.
Save zcaceres/f38b208a492e4dcd45f487638eff716c to your computer and use it in GitHub Desktop.
Child Routers in Express

Nested Routers in Express.js

Express makes it easy to nest routes in your routers. But I always had trouble accessing the request object's .params when you had a long URI with multiple parameters and nested routes.

Let's say you're building routes for a website www.music.com. Music is organized into albums with multiple tracks. Users can click to see a track list. Then they can select a single track and see a sub-page about that specific track.

At our application level, we could first have a Router to handle any requests to our albums.

const express = require('express');
const app = express();
const albumsRouter = require('./routers/albums');

//...

app.use('/albums', albumsRouter); // Forwards any requests to the /albums URI to our albums Router

//...

Inside our albums.js file...

const albums = require('express').Router();

//...

// Our app.use in the last file forwards a request to our albums router.
// So this route actually handles `/albums` because it's the root route when a request to /albums is forwarded to our router.
albums.get('/', function(req, res, next) {
  // res.send() our response here
});


// A route to handle requests to any individual album, identified by an album id
albums.get('/:albumId', function(req, res, next) {
  let myAlbumId = req.params.albumId;
  // get album data from server and res.send() a response here
});

//...

module.exports = albums;

So far, so good!

But what about when we add tracks to our routes? Since tracks are associated with a particular album, our routes will end up looking something like this:

www.music.com/albums/[AlbumIdHere]/tracks/[TrackIdHere]

In the spirit of Express' modular routers, we should have a separate router for tracks. That router isn't part of our top level application logic. We can nest it in our albums router instead.

First let's set up the proper forwarding in our albums router.

const albums = require('express').Router();
const tracks = require('./tracks').Router();

//...

// Our root route to /albums
albums.get('/', function(req, res, next) {
  // res.send() our response here
});

// A route to handle requests to any individual album, identified by an album id
albums.get('/:albumId', function(req, res, next) {
  let albumId = req.params.albumId;
  // retrieve album from database using albumId
  // res.send() response with album data
});

// Note, this route represents /albums/:albumId/tracks because our top-level router is already forwarding /albums to our Albums router!
albums.use('/:albumId/tracks', tracks);

//...

module.exports = albums;

Now let's make our tracks router in a track.js file.

const tracks = require('express').Router();

//...

// The root router for requests to our tracks path
track.get('/', function(req, res, next) {
  let albumId = req.params.albumId; // Our problem line

  // retrieve album's track data and render track list page
});

// The route for handling a request to a specific track
track.get('/:trackId', function(req, res, next) {
  let albumId = req.params.albumId; // <-- How do we get this?
  let trackId = req.params.trackId;

  // retrieve individual track data and render on single track page
});

//...

module.exports = tracks;

This seems like it should work. But wait a minute. To retrieve an album's track data, we'll need to know the album's id using the req.params.albumId that we set up in our albums route.

If you use the code above, you'll discover that our albumId is undefined. Our req.params does not have an albumId property inside our tracks router. Our router is broken because we can't figure out which album the user requested when our tracks router takes over.

Solve It

Express provides an elegant solution. Back in our albums router we can refactor the route that forwards requests to our nested tracks router.

Previously, we had:

albums.use('/:albumId/tracks', tracks);

Our refactoring will attach our req.params.albumdId to our req object directly. We'll next() the request, which tells Express to send the request to the next applicable route. But instead of allowing Express to choose the route, we'll include our optional third parameter to .use(), the router that we want Express to send the request to.

albums.use('/:albumId/tracks', function(req, res, next) {
  req.albumId = req.params.albumId;
  next()
}, tracks);

We've saved our param on our req object and told Express to send it directly to our tracks router.

Here's what our albums router will look like now.

const albums = require('express').Router();
const tracks = require('./tracks').Router();

//...

albums.get('/', function(req, res, next) {
  // send our response here
});

albums.get('/:albumId', function(req, res, next) {
  let albumId = req.params.albumId;
  // retrieve album from database using albumId
  // send response with album data
});

// The fix for our parameters problem
albums.use('/:albumId/tracks', function(req, res, next) {
  req.albumId = req.params.albumId;
  next()
}, tracks);

//...

module.exports = albums;

Now in tracks we can refactor to access the req.albumId property we set.

const tracks = require('express').Router();

//...

// The root router for requests to our tracks path
track.get('/', function(req, res, next) {
  let albumId = req.albumId; // Refactored to access our albumId property

  // retrieve album's track data and render track list page
});

// The route for handling a request to a specific track
track.get('/:trackId', function(req, res, next) {
  let albumId = req.albumId; // Refactored to access our albumId property!
  let trackId = req.params.trackId;

  // retrieve individual track data and render on single track page
});

//...

module.exports = tracks;

With everything refactored, we can access all the data we need.

Any time that we need to access a parameter from a parent router in its child, we can follow the same process.

  1. Attach the data we need from the parent router to our req object directly.
  2. next() in our parent router's .use() method.
  3. Set the optional third parameter in our parent's .use() as the child router that we want the request send to.

Happy routing!


@gcaraciolo
Copy link

gcaraciolo commented Apr 21, 2018

Thanks for sharing!

Actually, from express 4.5.0, it's possible to merge params!

See https://expressjs.com/en/4x/api.html#express.router

@crohit92
Copy link

Very nice article.
@gcaraciolo thanks alot for your quick tip

@tobloef
Copy link

tobloef commented Aug 11, 2019

Thanks, this had me stuck for a little while. I much prefer merging the parameters as @gcaraciolo suggested, cleaner that way.

@clemenskrenn
Copy link

Thank you for sharing.

@lucassimao
Copy link

man ... just use
const tracks = require('express').Router({ mergeParams: true });

@ouyuran
Copy link

ouyuran commented Sep 6, 2019

@gcaraciolo thanks very much

@katawo
Copy link

katawo commented Sep 24, 2019

thank you very much!

@gokturktopar
Copy link

It is a great article! Thanks for sharing

@jsvptf22
Copy link

jsvptf22 commented Jan 9, 2020

thanks , great article. but only run for me using { mergeParams: true } . how @gcaraciolo . thanks a lot

@nikitagupta0809
Copy link

Thanks! Awesome article. Helped a lot.

@anna-rmrf
Copy link

You have const tracks = require('express').Router();
and you are exporting module.exports = tracks;
yet you are doing const tracks = require('./tracks').Router(); ?????????

@ssoulless
Copy link

I think you have a spelling mistake, you have

track.get('/', function(req, res, next) { ....

but you defined your router as tracks it should be

tracks.get('/', function(req, res, next) { ....

@ssoulless
Copy link

I also think you need to replace

const tracks = require('./tracks').Router();

to just

const tracks = require('./tracks');

in your albums router.

@sanket-bhalerao
Copy link

this is useful, thanks.

@SrikanthKoti
Copy link

Great!! It's just what I needed, Thank you.

@jordanbmowry
Copy link

Just wanted to give two additional options for accessing the parent's route parameters.

const tracks= require("express").Router({ mergeParams: true });
//Express has a way to merge the router parameters from parent routes if necessary by passing in { mergeParams: true } to the router.

albums.use('/:albumId/tracks', function(req, res, next) {
res.locals.albumId = req.params.albumId;
next()
}, tracks);
//you now have access to the params on res.locals.albumId

@JamesIde
Copy link

Great article. Thanks for sharing this!

@Noushad-web
Copy link

Great!, I was looking something like this for my problem. Thank you so much.

@manzil-infinity180
Copy link

Nice Share 🤩

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment