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 main app, 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 RouteMount PathFull URL
router.get('/')/api/usersGET /api/users
router.get('/:id')/api/usersGET /api/users/42
router.post('/')/api/usersPOST /api/users
router.get('/')/api/productsGET /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 MethodRouter EquivalentWorks 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/ANo -- 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

  1. express.Router() creates an isolated mini-application with its own middleware and routes.
  2. router.use(mw) scopes middleware to only the routes on that router -- it does not leak to other routers.
  3. Mounting with app.use('/prefix', router) makes all router routes relative to the prefix.
  4. Use separate routers to enforce different middleware (public vs auth vs admin) cleanly.
  5. Nested routers with { mergeParams: true } handle deep URL hierarchies like /users/:id/addresses.
  6. A central router registry (routes/index.js) keeps server.js clean and the codebase navigable.

Explain-It Challenge

Explain without notes:

  1. How does router.use(middleware) differ from app.use(middleware) in terms of scope?
  2. What does { mergeParams: true } do when creating a router, and when do you need it?
  3. Sketch a project structure for an Express app with 4 resource types, showing where each router file lives and how they are mounted.
  4. You want auth on all routes except /auth/login and /auth/register. Describe the router architecture.

Navigation: <- 3.6.c Application-Level Middleware | 3.6.e -- Custom Middleware Patterns ->