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.
