Skip to content

Express Nested Routes

Many real-world resources belong to a parent, comments belong to posts, orders belong to users. This lesson shows how to model that relationship with nested Express routers and the mergeParams option.

What Are Nested Routes?

Nested routes express a parent-child relationship directly in the URL, /users/:userId/posts/:postId, meaning "the post with postId that belongs to the user with userId". Rather than repeating this whole path in one router, Express lets you mount a child router inside a parent one.

The tricky part is that, by default, a nested router cannot see the parent router's route parameters, that's what the mergeParams option solves.

// routes/posts.js
const express = require('express');
const router = express.Router({ mergeParams: true });

router.get('/', (req, res) => {
  res.json({ userId: req.params.userId, posts: [] });
});

module.exports = router;

// routes/users.js
const postsRouter = require('./posts');
router.use('/:userId/posts', postsRouter);

Without { mergeParams: true }, req.params.userId would be undefined inside posts.js, because parameters aren't shared across router boundaries by default.

Nested Router Syntax

const childRouter = express.Router({ mergeParams: true });

parentRouter.use('/:parentId/children', childRouter);

childRouter.get('/', (req, res) => {
  // req.params.parentId is available here
});
  • { mergeParams: true } makes a child router inherit req.params from its parent router.
  • Nesting can go more than one level deep for deeply hierarchical resources.
  • Each level of nesting typically lives in its own router file.
  • Nested routes still support their own independent middleware.

Nested Routes Cheatsheet

Common nested resource patterns and their URL shapes.

Relationship URL Pattern req.params (child)
User's posts /users/:userId/posts { userId }
Post's comments /users/:userId/posts/:postId/comments { userId, postId }
Org's projects /orgs/:orgId/projects/:projectId { orgId, projectId }

A Full Nested Example

Here is a complete, minimal example wiring a user router and a nested posts router together.

// posts.router.js
const express = require('express');
const router = express.Router({ mergeParams: true });

const posts = [{ id: 1, userId: 1, title: 'Hello' }];

router.get('/', (req, res) => {
  const userId = Number(req.params.userId);
  res.json(posts.filter((p) => p.userId === userId));
});

module.exports = router;

// users.router.js
const express = require('express');
const postsRouter = require('./posts.router');
const router = express.Router();

router.get('/:userId', (req, res) => res.json({ id: req.params.userId }));
router.use('/:userId/posts', postsRouter);

module.exports = router;

// app.js
app.use('/users', require('./routes/users.router'));

When to Avoid Deep Nesting

Nesting more than two or three levels deep (/a/:aId/b/:bId/c/:cId/d) usually makes URLs harder to read and clients harder to write against. When a sub-resource has a globally unique ID anyway, a flatter top-level route like /comments/:commentId is often simpler.

Common Mistakes

  • Forgetting { mergeParams: true } and then wondering why a parent's :userId is undefined in the child router.
  • Nesting resources so deeply that URLs become unreadable and hard for API clients to construct.
  • Repeating the parent's ID-lookup logic in every nested router instead of centralizing it with router.param().
  • Mixing up which router the mount path belongs to versus the paths defined inside the child router.

Key Takeaways

  • Nested routes model parent-child relationships directly in the URL structure.
  • { mergeParams: true } is required for a child router to read a parent router's route parameters.
  • Keep nesting shallow, two or three levels is usually the practical limit.
  • Each level of nesting can still have its own middleware and validation.

Pro Tip

If you find yourself needing mergeParams more than two levels deep, consider whether a flatter route with a unique ID (/comments/:id) would serve API consumers better than deep nesting.