Episode 3 — NodeJS MongoDB Backend Architecture / 3.6 — Middleware in Express
3.6.d — Router-Level Middleware
In one sentence: Router-level middleware is attached to an
express.Router()instance instead of the mainapp, giving you isolated mini-applications with their own middleware stacks that only affect routes mounted under that router -- the key to organizing large Express projects.
Navigation: <- 3.6.c Application-Level Middleware | 3.6.e -- Custom Middleware Patterns ->
1. express.Router() -- Creating Modular Route Groups
A Router is an isolated instance of middleware and routes. It behaves like a mini Express application -- it can have its own middleware, its own routes, and its own error handling.
const express = require('express');
const router = express.Router();
// Define routes on the router
router.get('/', (req, res) => {
res.json({ message: 'List all users' });
});
router.get('/:id', (req, res) => {
res.json({ message: `Get user ${req.params.id}` });
});
router.post('/', (req, res) => {
res.status(201).json({ message: 'Create user', data: req.body });
});
module.exports = router;
2. Mounting Routers with app.use()
You attach a router to a path prefix on the main app. All routes inside the router become relative to that prefix.
// server.js
const express = require('express');
const app = express();
const userRouter = require('./routes/users');
const productRouter = require('./routes/products');
app.use(express.json());
// Mount routers
app.use('/api/users', userRouter); // /api/users + router routes
app.use('/api/products', productRouter); // /api/products + router routes
app.listen(3000);
Resulting routes:
| Router Route | Mount Path | Full URL |
|---|---|---|
router.get('/') | /api/users | GET /api/users |
router.get('/:id') | /api/users | GET /api/users/42 |
router.post('/') | /api/users | POST /api/users |
router.get('/') | /api/products | GET /api/products |
3. router.use(middleware) -- Router-Scoped Middleware
Middleware registered with router.use() only runs for routes defined on that specific router. It does not affect other routers or the main app.
// routes/users.js
const express = require('express');
const router = express.Router();
// This middleware ONLY runs for user routes
router.use((req, res, next) => {
console.log(`[Users Router] ${req.method} ${req.baseUrl}${req.url}`);
next();
});
router.get('/', (req, res) => {
res.json({ users: [{ id: 1, name: 'Alice' }] });
});
router.get('/:id', (req, res) => {
res.json({ user: { id: req.params.id, name: 'Alice' } });
});
module.exports = router;
// routes/products.js
const express = require('express');
const router = express.Router();
// Different middleware -- only for product routes
router.use((req, res, next) => {
console.log(`[Products Router] ${req.method} ${req.baseUrl}${req.url}`);
next();
});
router.get('/', (req, res) => {
res.json({ products: [{ id: 1, name: 'Widget' }] });
});
module.exports = router;
Key point: The user router's middleware does not run when someone requests a product route, and vice versa.
4. Router as a Mini-Application
A router has the same API as an app for middleware and routing:
| App Method | Router Equivalent | Works the Same? |
|---|---|---|
app.use() | router.use() | Yes |
app.get() | router.get() | Yes |
app.post() | router.post() | Yes |
app.put() | router.put() | Yes |
app.delete() | router.delete() | Yes |
app.param() | router.param() | Yes |
app.listen() | N/A | No -- only app can listen |
const router = express.Router();
// Path-scoped middleware on the router
router.use('/admin', (req, res, next) => {
console.log('Admin area within this router');
next();
});
// Multiple middleware on a single route
router.get('/protected', authMiddleware, (req, res) => {
res.json({ secret: 'data' });
});
// Param middleware
router.param('id', (req, res, next, id) => {
// Runs for any route with :id parameter
req.resourceId = parseInt(id, 10);
if (isNaN(req.resourceId)) {
return res.status(400).json({ error: 'Invalid ID' });
}
next();
});
router.get('/:id', (req, res) => {
res.json({ id: req.resourceId }); // Already validated and parsed
});
5. Middleware Isolation -- Routers Don't Leak
One of the biggest advantages of router-level middleware is isolation. Middleware registered on one router has zero effect on other routers.
// server.js
const express = require('express');
const app = express();
app.use(express.json());
// --- Public Router (no auth) ---
const publicRouter = express.Router();
publicRouter.get('/health', (req, res) => res.json({ status: 'ok' }));
publicRouter.get('/docs', (req, res) => res.json({ docs: '...' }));
// --- Auth Router (auth required) ---
const authRouter = express.Router();
// Auth middleware ONLY applies to authRouter routes
authRouter.use((req, res, next) => {
const token = req.headers.authorization;
if (!token) return res.status(401).json({ error: 'Token required' });
// In real apps, verify the token here
req.user = { id: 1, name: 'Alice' }; // Decoded from token
next();
});
authRouter.get('/profile', (req, res) => {
res.json({ user: req.user });
});
authRouter.get('/settings', (req, res) => {
res.json({ settings: { theme: 'dark' } });
});
// --- Admin Router (admin auth required) ---
const adminRouter = express.Router();
adminRouter.use((req, res, next) => {
const token = req.headers.authorization;
if (!token) return res.status(401).json({ error: 'Token required' });
// Verify + check admin role
const user = { id: 1, role: 'admin' }; // Decoded from token
if (user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access only' });
}
req.user = user;
next();
});
adminRouter.get('/dashboard', (req, res) => {
res.json({ stats: { users: 500 } });
});
adminRouter.delete('/users/:id', (req, res) => {
res.json({ deleted: req.params.id });
});
// Mount all routers
app.use('/', publicRouter); // No auth needed
app.use('/api', authRouter); // Auth required
app.use('/admin', adminRouter); // Admin required
app.listen(3000);
Result:
GET /health --> No auth (public router)
GET /docs --> No auth (public router)
GET /api/profile --> Requires auth token (auth router)
GET /api/settings --> Requires auth token (auth router)
GET /admin/dashboard --> Requires admin token (admin router)
DELETE /admin/users/5 --> Requires admin token (admin router)
6. Shared vs Route-Specific Middleware
Shared: Applies to all routes in a router
const router = express.Router();
// Shared -- runs for every route in this router
router.use(logRequest);
router.use(validateContentType);
router.get('/', listItems);
router.post('/', createItem); // Both get logRequest + validateContentType
router.put('/:id', updateItem);
Route-specific: Applies to one route only
const router = express.Router();
// Only listItems is public -- no extra middleware
router.get('/', listItems);
// createItem requires auth + validation
router.post('/', authenticate, validateBody, createItem);
// updateItem requires auth + ownership check
router.put('/:id', authenticate, checkOwnership, updateItem);
// deleteItem requires auth + admin role
router.delete('/:id', authenticate, requireAdmin, deleteItem);
Combined pattern
const router = express.Router();
// Shared for all routes: logging
router.use(logRequest);
// Some routes are public
router.get('/', listItems);
router.get('/:id', getItem);
// Others need auth (applied per-route)
router.post('/', authenticate, createItem);
router.put('/:id', authenticate, checkOwnership, updateItem);
router.delete('/:id', authenticate, requireAdmin, deleteItem);
7. Real Example -- Auth Middleware Only on Protected Routes
A common pattern: one router for public endpoints, another for protected ones.
// middleware/auth.js
const jwt = require('jsonwebtoken');
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid or expired token' });
}
};
const requireRole = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: `Requires one of: ${roles.join(', ')}`
});
}
next();
};
};
module.exports = { authenticate, requireRole };
// routes/auth.js -- Public (no auth middleware on router)
const express = require('express');
const router = express.Router();
router.post('/register', (req, res) => {
// Create user logic
res.status(201).json({ message: 'User registered' });
});
router.post('/login', (req, res) => {
// Login logic, return JWT
res.json({ token: 'jwt-token-here' });
});
module.exports = router;
// routes/users.js -- Protected
const express = require('express');
const router = express.Router();
const { authenticate, requireRole } = require('../middleware/auth');
// All routes in this router require authentication
router.use(authenticate);
router.get('/me', (req, res) => {
res.json({ user: req.user });
});
router.put('/me', (req, res) => {
res.json({ message: 'Profile updated' });
});
// Admin-only routes
router.get('/', requireRole('admin'), (req, res) => {
res.json({ users: [] }); // List all users
});
router.delete('/:id', requireRole('admin'), (req, res) => {
res.json({ message: `User ${req.params.id} deleted` });
});
module.exports = router;
// server.js
const express = require('express');
const app = express();
app.use(express.json());
app.use('/auth', require('./routes/auth')); // Public
app.use('/users', require('./routes/users')); // Protected (auth on router)
app.listen(3000);
8. Organizing Large Applications with Multiple Routers
Project structure
project/
server.js
routes/
index.js <-- Central router registry
auth.js
users.js
products.js
orders.js
admin.js
middleware/
auth.js
validate.js
logger.js
Central router registry
// routes/index.js
const express = require('express');
const router = express.Router();
const authRoutes = require('./auth');
const userRoutes = require('./users');
const productRoutes = require('./products');
const orderRoutes = require('./orders');
const adminRoutes = require('./admin');
// Public routes
router.use('/auth', authRoutes);
// Protected routes (could add shared middleware here)
router.use('/users', userRoutes);
router.use('/products', productRoutes);
router.use('/orders', orderRoutes);
// Admin routes
router.use('/admin', adminRoutes);
module.exports = router;
// server.js
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Mount all routes under /api
app.use('/api', require('./routes'));
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// Error handler
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(3000, () => console.log('Server running on port 3000'));
Resulting URL structure:
POST /api/auth/login
POST /api/auth/register
GET /api/users/me
PUT /api/users/me
GET /api/products
GET /api/products/:id
POST /api/orders
GET /api/orders/:id
GET /api/admin/dashboard
9. Nested Routers
Routers can mount other routers for deep nesting.
// routes/users.js
const express = require('express');
const userRouter = express.Router();
const addressRouter = express.Router({ mergeParams: true }); // Important!
// Address routes (nested under users)
addressRouter.get('/', (req, res) => {
res.json({ message: `Addresses for user ${req.params.userId}` });
});
addressRouter.post('/', (req, res) => {
res.status(201).json({
message: `Address added for user ${req.params.userId}`,
address: req.body
});
});
// User routes
userRouter.get('/', (req, res) => {
res.json({ users: [] });
});
userRouter.get('/:userId', (req, res) => {
res.json({ user: { id: req.params.userId } });
});
// Mount address router under /:userId/addresses
userRouter.use('/:userId/addresses', addressRouter);
module.exports = userRouter;
// server.js
app.use('/api/users', require('./routes/users'));
// Results in:
// GET /api/users
// GET /api/users/42
// GET /api/users/42/addresses
// POST /api/users/42/addresses
{ mergeParams: true } is critical -- it allows the child router to access req.params.userId from the parent router.
10. req.baseUrl and req.path in Routers
When a router is mounted on a path, Express splits the URL:
// Mounted at /api/users
const router = express.Router();
router.get('/:id', (req, res) => {
// For request: GET /api/users/42
console.log(req.baseUrl); // '/api/users' (mount path)
console.log(req.path); // '/42' (route path within router)
console.log(req.originalUrl); // '/api/users/42' (full URL)
console.log(req.params.id); // '42'
res.json({
baseUrl: req.baseUrl,
path: req.path,
originalUrl: req.originalUrl
});
});
Key Takeaways
express.Router()creates an isolated mini-application with its own middleware and routes.router.use(mw)scopes middleware to only the routes on that router -- it does not leak to other routers.- Mounting with
app.use('/prefix', router)makes all router routes relative to the prefix. - Use separate routers to enforce different middleware (public vs auth vs admin) cleanly.
- Nested routers with
{ mergeParams: true }handle deep URL hierarchies like/users/:id/addresses. - A central router registry (
routes/index.js) keepsserver.jsclean and the codebase navigable.
Explain-It Challenge
Explain without notes:
- How does
router.use(middleware)differ fromapp.use(middleware)in terms of scope? - What does
{ mergeParams: true }do when creating a router, and when do you need it? - Sketch a project structure for an Express app with 4 resource types, showing where each router file lives and how they are mounted.
- You want auth on all routes except
/auth/loginand/auth/register. Describe the router architecture.
Navigation: <- 3.6.c Application-Level Middleware | 3.6.e -- Custom Middleware Patterns ->