Express.js Express Router Module

As an application grows, putting every route in one app.js file makes the code long and hard to maintain. Express provides a Router class that lets you split routes into separate files organized by feature. Each file handles its own group of routes, and your main app file simply connects them together. This keeps your codebase clean, organized, and easy to navigate.

The Apartment Building Analogy

Think of your application as an apartment building. Without a router, the building manager handles every request for every apartment personally. With Express Router, you hire a separate manager for each floor: one for users, one for products, one for orders. The head manager (your main app) simply directs visitors to the right floor manager, who handles the rest.

app.js (Head Manager)
      │
      ├── /api/users → userRouter (User Floor Manager)
      │       ├── GET  /api/users
      │       ├── POST /api/users
      │       └── GET  /api/users/:id
      │
      ├── /api/products → productRouter (Product Floor Manager)
      │       ├── GET  /api/products
      │       ├── POST /api/products
      │       └── DELETE /api/products/:id
      │
      └── /api/orders → orderRouter (Order Floor Manager)
              ├── GET  /api/orders
              └── POST /api/orders

Without Router: The Messy Single File

Here is what a growing application looks like without routing separation:

// app.js — gets messy fast
app.get('/api/users', ...);
app.post('/api/users', ...);
app.get('/api/users/:id', ...);
app.put('/api/users/:id', ...);
app.delete('/api/users/:id', ...);
app.get('/api/products', ...);
app.post('/api/products', ...);
app.get('/api/products/:id', ...);
// ... hundreds more lines

Project Structure with Router

my-express-app/
│
├── routes/
│   ├── users.js        ← All user-related routes
│   ├── products.js     ← All product-related routes
│   └── orders.js       ← All order-related routes
│
├── controllers/
│   ├── userController.js
│   └── productController.js
│
├── models/
│   ├── User.js
│   └── Product.js
│
└── app.js              ← Clean main file

Create a Router File

Create routes/users.js:

const express = require('express');
const router = express.Router(); // Create a router instance

// GET /api/users — list all users
router.get('/', (req, res) => {
  res.json({ message: 'Get all users' });
});

// GET /api/users/:id — get one user
router.get('/:id', (req, res) => {
  res.json({ message: `Get user ${req.params.id}` });
});

// POST /api/users — create a user
router.post('/', (req, res) => {
  res.status(201).json({ message: 'User created', data: req.body });
});

// PUT /api/users/:id — update a user
router.put('/:id', (req, res) => {
  res.json({ message: `Update user ${req.params.id}` });
});

// DELETE /api/users/:id — delete a user
router.delete('/:id', (req, res) => {
  res.json({ message: `Delete user ${req.params.id}` });
});

module.exports = router; // Export the router

Notice that the paths inside this file do not include /api/users. The prefix gets added in the main app file. Paths here are relative to where you mount the router.

Register the Router in app.js

const express = require('express');
const userRouter = require('./routes/users');
const productRouter = require('./routes/products');

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

// Mount routers at their base paths
app.use('/api/users', userRouter);
app.use('/api/products', productRouter);

app.listen(3000, () => {
  console.log('Server running');
});

Now all routes defined in routes/users.js automatically get the /api/users prefix. A route defined as router.get('/:id', ...) responds to GET /api/users/:id.

How Mounting Works

Router file defines:      Mounted at:         Full URL served:
──────────────────────────────────────────────────────────────
router.get('/')        + /api/users    →  GET /api/users
router.get('/:id')     + /api/users    →  GET /api/users/:id
router.post('/')       + /api/users    →  POST /api/users
router.delete('/:id')  + /api/users    →  DELETE /api/users/:id

Separating Controllers from Routes

Route handlers can grow long. Move the handler logic into a separate controller file. The route file only maps URLs to controller functions:

// controllers/userController.js
const getAllUsers = (req, res) => {
  res.json({ message: 'Returning all users from database' });
};

const getUserById = (req, res) => {
  res.json({ message: `Fetching user ${req.params.id}` });
};

const createUser = (req, res) => {
  res.status(201).json({ message: 'User created', data: req.body });
};

module.exports = { getAllUsers, getUserById, createUser };
// routes/users.js
const express = require('express');
const router = express.Router();
const { getAllUsers, getUserById, createUser } = require('../controllers/userController');

router.get('/', getAllUsers);
router.get('/:id', getUserById);
router.post('/', createUser);

module.exports = router;

Each file now has a single responsibility: routes map URLs, controllers hold logic.

Router-Level Middleware

Attach middleware to a router so it runs for every route in that file:

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

// This middleware runs for ALL routes in this file
router.use((req, res, next) => {
  const token = req.headers['admin-token'];
  if (token !== 'supersecret') {
    return res.status(403).json({ error: 'Admin access denied' });
  }
  next();
});

router.get('/dashboard', (req, res) => {
  res.json({ message: 'Welcome to admin dashboard' });
});

router.get('/users', (req, res) => {
  res.json({ message: 'Admin user list' });
});

module.exports = router;

Nesting Routers

Routers can mount other routers for deeply nested routes:

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

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

module.exports = router;
// routes/users.js
const postRouter = require('./posts');

router.use('/:userId/posts', postRouter);
// Now: GET /api/users/42/posts works

Full Route Map After Splitting

app.js
  app.use('/api/users', userRouter)      → routes/users.js
  app.use('/api/products', productRouter) → routes/products.js
  app.use('/api/orders', orderRouter)     → routes/orders.js
  app.use('/admin', adminRouter)          → routes/admin.js (with auth middleware)

Final URLs:
GET    /api/users            List users
GET    /api/users/42         Get user 42
POST   /api/users            Create user
GET    /api/users/42/posts   Get posts by user 42
GET    /api/products         List products
GET    /admin/dashboard      Admin dashboard (protected)

Summary

express.Router() creates a mini application that handles a group of related routes. Create one router file per feature (users, products, orders), define routes relative to the base path, export the router, and mount it in app.js with app.use('/api/users', userRouter). Move handler logic into controller files so route files stay short and focused on URL mapping. Add router-level middleware with router.use() to apply checks to every route in a file. Nest routers for hierarchical URLs like /users/:id/posts.

Leave a Comment

Your email address will not be published. Required fields are marked *