Skip to content

Router-Level Middleware

Router-level middleware works exactly like application-level middleware, but is bound to an express.Router() instance instead of the main app object. This lesson shows how to scope middleware to a specific group of routes.

What Is Router-Level Middleware?

express.Router() creates a mini, mountable application, complete with its own middleware and routes. Calling router.use() on it registers middleware that runs only for requests handled by that specific router, not the whole app.

This is the standard way to scope logic, like auth checks or validation, to one resource's routes without leaking it into unrelated ones.

const express = require('express');
const router = express.Router();

router.use((req, res, next) => {
  console.log('Order router hit:', req.method, req.url);
  next();
});

router.get('/', listOrders);
router.get('/:id', getOrder);

module.exports = router;

This logging middleware only runs for requests reaching this router, mounted elsewhere with app.use('/orders', router).

router.use() Syntax

const router = express.Router();

router.use(callback);
router.use(path, callback);
router.get(path, handler);

app.use('/mount-path', router);
  • router.use() mirrors app.use(), but only applies within that router.
  • The router itself is mounted onto the app with app.use(mountPath, router).
  • Paths defined inside the router are relative to its mount path.
  • Multiple routers can each have their own independent middleware stack.

Router Middleware Cheatsheet

Router-scoped middleware patterns you will reuse across resource files.

Pattern Example Effect
Log within router router.use(logger) Only logs requests this router handles
Auth within router router.use(requireAuth) Protects every route on this router
Path-scoped in router router.use('/:id', loadResource) Runs before any /:id route below it
Mount router app.use('/orders', ordersRouter) Attaches the whole router at /orders

Per-Resource Middleware

A common pattern is to load a resource once (say, an order) in router-level middleware, attach it to req, and let every route below reuse it without repeating the lookup.

const router = express.Router();

router.param('id', (req, res, next, id) => {
  const order = orders.find((o) => o.id === Number(id));
  if (!order) return res.status(404).json({ error: 'Order not found' });
  req.order = order;
  next();
});

router.get('/:id', (req, res) => res.json(req.order));
router.delete('/:id', (req, res) => {
  orders = orders.filter((o) => o.id !== req.order.id);
  res.status(204).end();
});

module.exports = router;

Combining Router and Application Middleware

A request first passes through any matching application-level middleware, then, once it reaches a mounted router, through that router's own middleware, before finally reaching a specific route.

app.use(express.json());        // application-level
app.use('/orders', requireAuth); // application-level, path-scoped

const ordersRouter = express.Router();
ordersRouter.use(logger);        // router-level
app.use('/orders', ordersRouter);

Common Mistakes

  • Forgetting to mount the router with app.use(), none of its routes or middleware ever run.
  • Registering router-level middleware after the routes on that same router that need it.
  • Duplicating the same auth check in application middleware and again in every router, when one is enough.
  • Assuming router-level middleware sees requests for routes mounted on a completely different router.

Key Takeaways

  • Router-level middleware is registered with router.use() on an express.Router() instance.
  • It only runs for requests dispatched to that specific router after mounting.
  • It is the standard way to scope logic to one resource's set of routes.
  • router.param() centralizes per-parameter loading logic within a router.

Pro Tip

Keep each resource's router, middleware, and routes in its own file (orders.router.js), so opening one file shows you everything that can happen to an /orders request.