One-Off Middleware Without Global Mounts
Per-Route Middleware
Add middleware to a single route — auth, validation, role checks — without leaking it elsewhere.
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: requireAuth → requireAdmin → listUsers.
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.