Per-Route Middleware

One-Off Middleware Without Global Mounts

Per-Route Middleware

Add middleware to a single route — auth, validation, role checks — without leaking it elsewhere.

2 min read Level 1/5 #express#middleware#routes
What you'll learn
  • Use middleware on a specific route
  • Compose middleware arrays for reuse
  • Keep route files readable

Not every middleware needs to be global. Pass them inline to a specific route — (path, ...middleware, handler).

Inline

app.get("/admin/users", requireAuth, requireAdmin, listUsers);

The route’s chain: requireAuthrequireAdminlistUsers.

Arrays

const adminOnly = [requireAuth, requireAdmin];

app.get("/admin/users",     adminOnly, listUsers);
app.post("/admin/users",    adminOnly, createUser);
app.delete("/admin/users/:id", adminOnly, deleteUser);

Define the bundle once, apply many times.

Validation Per Route

import { z } from "zod";

const validate = (schema) => (req, res, next) => {
  const r = schema.safeParse(req.body);
  if (!r.success) {
    return res.status(400).json({ error: r.error.issues });
  }
  req.validBody = r.data;
  next();
};

const Login = z.object({ email: z.string().email(), password: z.string().min(8) });

app.post("/auth/login", validate(Login), loginHandler);

Per-Method Middleware

A common pattern — different rules per HTTP method:

app.route("/api/posts/:id")
  .all(loadPost)                 // shared
  .get(getPost)
  .patch(requireAuth, updatePost)
  .delete(requireAuth, requireAdmin, deletePost);

loadPost runs for all methods; auth applies only to mutating ones.

Where To Mount

Per-route is the right choice when:

  • The middleware only applies to a few routes
  • Mounting globally would slow down or break unrelated routes
  • The route file is the natural place to read what runs

Per-route lives in the routes file. Global middleware lives in app.js. Same separation as routes vs glue.

Real-World Patterns →