Router & Modular Routes

intermediate express router structure

Putting every route in one index.js works until it doesn’t — by route 50, the file is 800 lines and impossible to navigate. express.Router() is how we split routes into modules.

In simple language — a Router is a mini Express app. It has the same .get/.post/.use methods, but instead of starting a server it gets “mounted” onto the main app at some path prefix.

The basic pattern

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

router.get('/', (req, res) => {
  res.json([{ id: 1, name: 'Manish' }]);
});

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

router.post('/', (req, res) => {
  res.status(201).json({ created: true });
});

module.exports = router;
// app.js
const express = require('express');
const usersRouter = require('./routes/users');

const app = express();
app.use(express.json());
app.use('/api/users', usersRouter);

app.listen(3000);

Now GET /api/users/42 lands in the router’s /:id handler. The mount path (/api/users) gets stripped before the router sees the URL.

Mounting tree
app
├── /api
│   ├── /users ─→ usersRouter
│   │   ├── GET    /         (list)
│   │   ├── GET    /:id      (get one)
│   │   └── POST   /         (create)
│   ├── /posts ─→ postsRouter
│   │   ├── GET    /
│   │   └── POST   /
│   └── /auth  ─→ authRouter
│       ├── POST /login
│       └── POST /logout
└── GET /healthz
    

Why this matters

  • Separation of concerns — one file per resource (users, posts, auth)
  • Per-router middleware — apply auth only to certain routes
  • Reusability — mount the same router under multiple paths
  • Testability — import the router directly and test in isolation

Per-router middleware

router.use() works just like app.use() but only affects that router:

// routes/admin.js
const router = express.Router();

const requireAdmin = (req, res, next) => {
  if (req.user?.role !== 'admin') return res.sendStatus(403);
  next();
};

router.use(requireAdmin);  // every route below requires admin

router.get('/stats', (req, res) => res.json({ users: 1000 }));
router.delete('/users/:id', (req, res) => res.sendStatus(204));

module.exports = router;

Mount it at /admin and now every /admin/* request runs requireAdmin first. Clean.

mergeParams — passing params down

A subtle one. If we have nested resources like /users/:userId/posts, the posts router doesn’t see :userId by default. We need mergeParams: true:

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

router.use('/:userId/posts', postsRouter);
// routes/posts.js
const router = express.Router({ mergeParams: true });

router.get('/', (req, res) => {
  res.json({ userId: req.params.userId }); // works only with mergeParams
});

module.exports = router;

Without mergeParams, req.params.userId is undefined inside the posts router. Easy to get bitten by this.

A realistic layout

src/
├── app.js
├── routes/
│   ├── index.js          # mounts all sub-routers
│   ├── users.js
│   ├── posts.js
│   └── auth.js
├── middleware/
│   ├── auth.js
│   └── errorHandler.js
└── controllers/          # actual business logic
    ├── users.js
    └── posts.js
// routes/index.js
const express = require('express');
const router = express.Router();

router.use('/users', require('./users'));
router.use('/posts', require('./posts'));
router.use('/auth', require('./auth'));

module.exports = router;
// app.js
app.use('/api', require('./routes'));

Now app.js has one route line. Adding a new resource means creating one file and adding one router.use.

Routes vs controllers

A common pattern — the router defines URLs and delegates to a controller function:

// routes/users.js
const router = express.Router();
const ctrl = require('../controllers/users');

router.get('/', ctrl.list);
router.get('/:id', ctrl.get);
router.post('/', ctrl.create);

module.exports = router;

This keeps the router file readable as a “URL map” and pushes logic into testable controller functions. Worth doing once the codebase grows past a few files.