Router-Level Middleware
Router-level middleware works exactly like application-level middleware, but it's bound to an express.Router() instance instead of the app. This lets you scope cross-cutting logic — auth, validation, logging — to a specific group of routes rather than the whole application. Combined with router modules, it's how large apps keep concerns local and organized.
Middleware on a router
routes/users.js
import { Router } from 'express'
const router = Router()
// Runs for EVERY route in this router only:
router.use((req, res, next) => {
console.log('users router:', req.method, req.originalUrl)
next()
})
router.get('/', listUsers)
router.get('/:id', getUser)
export default routerThe killer use case: scoped auth
Protect an entire section of your API by putting auth middleware at the top of a router — one line guards every route below it:
routes/admin.js
import { Router } from 'express'
const router = Router()
// Gate the whole router — runs before every admin route:
router.use((req, res, next) => {
if (!req.user) return res.status(401).json({ error: 'Login required' })
if (!req.user.isAdmin) return res.status(403).json({ error: 'Forbidden' })
next()
})
router.get('/stats', getStats) // all protected automatically
router.get('/users', getAllUsers)
router.delete('/users/:id', deleteUser)
export default routerPer-route middleware
You can also attach middleware to a single route by listing it before the handler — ideal when only some routes need it:
function validateBody(req, res, next) {
if (!req.body.title) return res.status(422).json({ error: 'title required' })
next()
}
// Only this route runs validateBody:
router.post('/', validateBody, createPost)
// Multiple per-route middleware run left to right:
router.put('/:id', requireAuth, validateBody, updatePost)Mounting middleware-only routers
A router can be purely middleware — useful for applying a stack of concerns to a path prefix without defining routes in that file:
import { Router } from 'express'
import rateLimit from 'express-rate-limit'
const apiGuards = Router()
apiGuards.use(rateLimit({ windowMs: 60_000, max: 100 }))
apiGuards.use(requireApiKey)
// Apply the whole guard stack to everything under /api:
app.use('/api', apiGuards)
app.use('/api/users', usersRouter) // these now sit behind the guardsrouter.param for resource loading
A router-scoped router.param runs whenever a given parameter appears in that router — the clean place to load-and-attach a resource (covered in route parameters):
router.param('id', async (req, res, next, id) => {
const user = await db.users.find(Number(id))
if (!user) return res.status(404).json({ error: 'User not found' })
req.user = user
next()
})
router.get('/:id', (req, res) => res.json(req.user)) // req.user ready
router.put('/:id', (req, res) => res.json(update(req.user, req.body)))App vs router middleware
Application-level | Router-level | |
|---|---|---|
Bound to |
| A |
Scope | Whole app | That router's mount path |
Registered with |
|
|
Best for | Global concerns (logging, body parse) | Resource-specific concerns (auth, loading) |