When your Express API has five or ten routes, keeping them all in index.js is manageable. When it has fifty โ covering posts, users, auth, comments, and file uploads โ a single file becomes a maintenance nightmare. express.Router() is the solution: it lets you create mini Express applications that handle a subset of your routes, which you then mount on a path prefix in your main app. The result is a clean, modular project where each resource has its own dedicated route file, its own controller, and its own logical boundary. In this lesson you will refactor a single-file server into a properly structured Express application.
What express.Router() Provides
| Feature | Detail |
|---|---|
| Isolated route scope | Routes defined on a Router only match within its mounted path |
| Own middleware stack | Middleware added to a Router only applies to that Router’s routes |
| Mountable at any prefix | app.use('/api/posts', postsRouter) โ all router routes become /api/posts/... |
| Composable | Routers can be nested โ a router can mount another router |
| Same API as app | All app.get/post/put/delete/use methods work on Router too |
app.use('/api/posts', postsRouter), the prefix is stripped before matching inside the Router. So router.get('/', handler) inside the Router matches GET /api/posts, and router.get('/:id', handler) matches GET /api/posts/:id. You do not repeat /api/posts inside the Router file.app with app.use() before mounting any routers. Middleware added to a Router after the Router is mounted on app will not run for requests that matched an earlier Router.Before โ Everything in index.js
// server/index.js โ hard to maintain at scale
require('dotenv').config();
const express = require('express');
const app = express();
app.use(express.json());
// Posts routes (mixed in with everything else)
app.get('/api/posts', async (req, res) => { /* ... */ });
app.post('/api/posts', async (req, res) => { /* ... */ });
app.get('/api/posts/:id', async (req, res) => { /* ... */ });
app.put('/api/posts/:id', async (req, res) => { /* ... */ });
app.delete('/api/posts/:id', async (req, res) => { /* ... */ });
// Auth routes
app.post('/api/auth/register', async (req, res) => { /* ... */ });
app.post('/api/auth/login', async (req, res) => { /* ... */ });
// User routes
app.get('/api/users/:id', async (req, res) => { /* ... */ });
app.patch('/api/users/:id', async (req, res) => { /* ... */ });
app.listen(5000);
After โ Modular Router Files
// โโ server/src/routes/posts.js โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const express = require('express');
const router = express.Router();
const {
getAllPosts,
getPostById,
createPost,
updatePost,
deletePost,
} = require('../controllers/postController');
const protect = require('../middleware/auth');
// GET /api/posts and POST /api/posts
router.route('/')
.get(getAllPosts)
.post(protect, createPost);
// GET /api/posts/:id PUT /api/posts/:id DELETE /api/posts/:id
router.route('/:id')
.get(getPostById)
.put(protect, updatePost)
.delete(protect, deletePost);
module.exports = router;
// โโ server/src/routes/auth.js โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const express = require('express');
const router = express.Router();
const { register, login, getMe } = require('../controllers/authController');
const protect = require('../middleware/auth');
router.post('/register', register);
router.post('/login', login);
router.get('/me', protect, getMe);
module.exports = router;
// โโ server/src/routes/users.js โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const express = require('express');
const router = express.Router();
const { getUserProfile, updateProfile } = require('../controllers/userController');
const protect = require('../middleware/auth');
router.get('/:id', getUserProfile);
router.patch('/:id', protect, updateProfile);
module.exports = router;
// โโ server/index.js โ clean and minimal โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const connectDB = require('./src/config/db');
const app = express();
const PORT = process.env.PORT || 5000;
connectDB();
// โโ Global middleware โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use(helmet());
app.use(cors({ origin: process.env.CLIENT_URL || 'http://localhost:5173' }));
app.use(express.json());
// โโ Mount routers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use('/api/posts', require('./src/routes/posts'));
app.use('/api/auth', require('./src/routes/auth'));
app.use('/api/users', require('./src/routes/users'));
// โโ 404 and error handlers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use(require('./src/middleware/notFound'));
app.use(require('./src/middleware/errorHandler'));
app.listen(PORT, () => console.log(`Server โ http://localhost:${PORT}`));
Controller Pattern โ Separating Logic from Routes
// โโ server/src/controllers/postController.js โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const Post = require('../models/Post');
// @desc Get all posts
// @route GET /api/posts
// @access Public
const getAllPosts = async (req, res, next) => {
try {
const { page = 1, limit = 10, tag, published } = req.query;
const filter = {};
if (tag) filter.tags = tag;
if (published) filter.published = published === 'true';
const posts = await Post.find(filter)
.sort({ createdAt: -1 })
.skip((page - 1) * limit)
.limit(parseInt(limit, 10));
res.json({ success: true, count: posts.length, data: posts });
} catch (err) {
next(err);
}
};
// @desc Get single post
// @route GET /api/posts/:id
// @access Public
const getPostById = async (req, res, next) => {
try {
const post = await Post.findById(req.params.id).populate('author', 'name avatar');
if (!post) return res.status(404).json({ success: false, message: 'Post not found' });
res.json({ success: true, data: post });
} catch (err) {
next(err);
}
};
module.exports = { getAllPosts, getPostById }; // export all handlers
Final Project Structure
server/
โโโ index.js โ entry point โ mounts routers, starts server
โโโ .env
โโโ nodemon.json
โโโ src/
โโโ config/
โ โโโ db.js โ Mongoose connection
โโโ models/
โ โโโ Post.js
โ โโโ User.js
โโโ controllers/
โ โโโ postController.js โ getAllPosts, getPostById, createPost...
โ โโโ authController.js โ register, login, getMe
โ โโโ userController.js โ getUserProfile, updateProfile
โโโ routes/
โ โโโ posts.js โ router.get('/', ...) router.get('/:id', ...)
โ โโโ auth.js
โ โโโ users.js
โโโ middleware/
โโโ auth.js โ JWT protect middleware
โโโ notFound.js โ 404 handler
โโโ errorHandler.js โ global error middleware
Common Mistakes
Mistake 1 โ Repeating the mount path inside the Router
โ Wrong โ the prefix is already added by app.use():
// routes/posts.js
router.get('/api/posts', getAllPosts); // results in /api/posts/api/posts
router.get('/api/posts/:id', getPostById); // results in /api/posts/api/posts/:id
โ Correct โ Router paths are relative to the mount point:
router.get('/', getAllPosts); // matches GET /api/posts โ
router.get('/:id', getPostById); // matches GET /api/posts/:id โ
Mistake 2 โ Putting business logic directly in route files
โ Wrong โ database queries inside the route file blur responsibilities:
// routes/posts.js
router.get('/', async (req, res) => {
const posts = await Post.find().sort({ createdAt: -1 }).limit(10);
res.json(posts);
}); // route file now has business logic โ hard to test, hard to reuse
โ Correct โ keep route files as thin wiring; put logic in controller files.
Mistake 3 โ Forgetting to export the router
โ Wrong โ router is never exported so index.js gets undefined:
// routes/posts.js
const router = express.Router();
router.get('/', handler);
// forgot: module.exports = router;
// index.js
app.use('/api/posts', require('./src/routes/posts')); // undefined โ crash
โ Correct โ always export the router at the bottom of every route file:
module.exports = router; // โ
Quick Reference
| Task | Code |
|---|---|
| Create a router | const router = express.Router() |
| Define route on router | router.get('/', handler) |
| Chain methods on router | router.route('/').get(h1).post(h2) |
| Export router | module.exports = router |
| Mount router in app | app.use('/api/posts', require('./routes/posts')) |
| Router-level middleware | router.use(protect) |