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.
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.
- Attach the data we need from the parent router to our
req
object directly. next()
in our parent router's.use()
method.- Set the optional third parameter in our parent's
.use()
as the child router that we want the request send to.
Happy routing!