Express Router

Difficulty: Beginner

As your Express application grows, defining all routes directly on the app object becomes unmanageable. Express Router provides a way to create modular, mountable route handlers. A Router instance is a complete middleware and routing system - often called a 'mini-app'. You can define routes and middleware on a Router, then mount the entire Router onto the main app at a specific path prefix.

To create a router, call express.Router() which returns a new router instance. You define routes on this instance using the same methods as the app object: router.get(), router.post(), router.put(), router.delete(), etc. Then you mount the router onto your app with app.use('/prefix', router). All routes defined on the router become relative to this prefix. For example, if you mount a router at '/api/users' and define router.get('/'), that route matches GET /api/users.

The standard pattern is to create one router file per resource or feature. A users router handles /api/users routes, a products router handles /api/products routes, and so on. A central routes/index.js file imports all routers and mounts them with their prefixes. This keeps each file focused and small, making the codebase easier to navigate.

Routers can also have their own middleware using router.use(). This middleware only applies to routes defined on that specific router, not to the entire application. For example, you might apply authentication middleware only to the users router while keeping the public product routes open. This is much cleaner than adding auth checks to every individual route handler.

Routers can be nested: a router can mount another router. This enables deeply structured URL hierarchies like /api/users/:userId/posts/:postId/comments. Each level can have its own router with its own middleware, keeping the code organized and the concerns separated. The mergeParams option allows child routers to access parameters defined by parent routers.

Code examples

Basic Router Setup

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

let users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
];

// These routes are relative to where the router is mounted
// If mounted at '/api/users', router.get('/') = GET /api/users
router.get('/', (req, res) => {
  res.json(users);
});

router.get('/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ error: 'User not found' });
  res.json(user);
});

router.post('/', (req, res) => {
  const user = { id: users.length + 1, name: req.body.name };
  users.push(user);
  res.status(201).json(user);
});

module.exports = router;

// --- app.js ---
const express = require('express');
const usersRouter = require('./routes/users.routes');

const app = express();
app.use(express.json());

// Mount the router at /api/users
app.use('/api/users', usersRouter);

app.listen(3000, () => console.log('Server on port 3000'));

The users router defines routes relative to its mount point. router.get('/') becomes GET /api/users, and router.get('/:id') becomes GET /api/users/:id. The router file exports only route definitions, keeping it clean and focused.

Multiple Routers with Central Aggregation

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

router.get('/', (req, res) => res.json([{ id: 1, name: 'Alice' }]));
router.post('/', (req, res) => res.status(201).json({ id: 2, ...req.body }));

module.exports = router;

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

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

module.exports = router;

// --- routes/index.js (route aggregator) ---
const express = require('express');
const router = express.Router();

const usersRouter = require('./users.routes');
const productsRouter = require('./products.routes');

router.use('/users', usersRouter);
router.use('/products', productsRouter);

module.exports = router;

// --- app.js ---
const express = require('express');
const routes = require('./routes');

const app = express();
app.use(express.json());
app.use('/api', routes);  // All routes under /api

app.listen(3000, () => console.log('Server on port 3000'));

The route aggregator (routes/index.js) imports all feature routers and mounts them with their prefixes. The main app mounts the aggregator at /api. This creates a clean hierarchy: /api/users, /api/products. Adding a new feature just means creating a new router file and adding one line to the aggregator.

Router-level Middleware and Nested Routers

const express = require('express');
const app = express();
app.use(express.json());

// Simulated auth middleware
function requireAuth(req, res, next) {
  const token = req.headers.authorization;
  if (!token) return res.status(401).json({ error: 'Authentication required' });
  req.user = { id: 1, name: 'Alice' }; // Simulated user
  next();
}

// Public routes - no auth required
const publicRouter = express.Router();
publicRouter.get('/products', (req, res) => {
  res.json([{ id: 1, name: 'Laptop' }]);
});

// Protected routes - auth required for ALL routes on this router
const protectedRouter = express.Router();
protectedRouter.use(requireAuth); // Applied to all routes below

protectedRouter.get('/profile', (req, res) => {
  res.json({ user: req.user });
});

protectedRouter.get('/settings', (req, res) => {
  res.json({ theme: 'dark', user: req.user });
});

// Nested router with mergeParams
const commentsRouter = express.Router({ mergeParams: true });
commentsRouter.get('/', (req, res) => {
  res.json({
    postId: req.params.postId,
    comments: [{ id: 1, text: 'Great post!' }]
  });
});

const postsRouter = express.Router();
postsRouter.get('/', (req, res) => res.json([{ id: 1, title: 'My Post' }]));
postsRouter.use('/:postId/comments', commentsRouter); // Nested!

// Mount everything
app.use('/api', publicRouter);
app.use('/api', protectedRouter);
app.use('/api/posts', postsRouter);

app.listen(3000, () => console.log('Server on port 3000'));

router.use(requireAuth) applies auth middleware to all routes on that router. The comments router uses { mergeParams: true } to access :postId from the parent router. GET /api/posts/1/comments returns comments for post 1.

Key points

Concepts covered

express.Router, Modular Routes, router.use, Mounting Routers, Route Prefixes, Route Organization