Skip to content

Express Middleware Patterns — Complete Guide with Examples

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Express Middleware Patterns. We cover key concepts, practical examples, and best practices to help you master this topic.

Express middleware patterns give you fine-grained control over request processing through application-level, router-level, and error-handling middleware, each serving a distinct purpose in your web application.

What You'll Learn

By the end of this tutorial, you will understand the four types of Express middleware, when to use each type, and how to combine them for clean, maintainable code.

Why It Matters

Express is the most popular Node.js framework, and middleware is its core architectural pattern. Mastering Express middleware lets you build secure, performant APIs the same way DodaTech builds its production services.

Real-World Use

The DodaZIP file conversion API uses Express middleware at the application level for CORS and compression, at the router level for API Versioning, and at the error level for consistent error responses.

Express Middleware Types

flowchart LR
  A[Express Middleware] --> B[Application-level]
  A --> C[Router-level]
  A --> D[Error-handling]
  A --> E[Built-in]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

Application-Level Middleware

Application-level middleware is bound to the app object using app.use() or app.VERB(). It runs on every request or every matching request.

const express = require("express");
const app = express();

// Runs on every request
app.use((req, res, next) => {
  req.requestTime = Date.now();
  next();
});

// Runs only on GET /hello
app.get("/hello", (req, res) => {
  res.json({ message: "Hello", time: req.requestTime });
});

app.listen(3000);

Expected output for GET /hello:

{"message": "Hello", "time": 1722189000000}

Router-Level Middleware

Router-level middleware works identically to application-level middleware but is bound to an instance of express.Router(). This lets you group routes and their middleware together.

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

function validateId(req, res, next) {
  const id = Number(req.params.id);
  if (isNaN(id)) {
    return res.status(400).json({ error: "Invalid ID" });
  }
  req.id = id;
  next();
}

router.get("/users/:id", validateId, (req, res) => {
  res.json({ userId: req.id });
});

const app = express();
app.use("/api", router);
app.listen(3000);

Expected output for GET /api/users/abc:

{"error": "Invalid ID"}

Expected output for GET /api/users/42:

{"userId": 42}

Error-Handling Middleware

Error-handling middleware has four parameters instead of three: err, req, res, next. Express recognizes it by the four parameters and calls it when any middleware or route throws an error or calls next(err).

const express = require("express");
const app = express();

app.get("/data", (req, res, next) => {
  try {
    const data = JSON.parse('{invalid json}');
    res.json(data);
  } catch (err) {
    next(err);
  }
});

app.use((err, req, res, next) => {
  console.error("Error caught:", err.message);
  res.status(500).json({
    error: "Something went wrong",
    details: err.message
  });
});

app.listen(3000);

Expected output:

{"error": "Something went wrong", "details": "Unexpected token i in JSON at position 1"}

Built-in Middleware

Express provides several built-in middleware functions that handle common tasks.

const express = require("express");
const app = express();

// Parse JSON bodies
app.use(express.json());

// Parse URL-encoded bodies
app.use(express.urlencoded({ extended: true }));

// Serve static files
app.use(express.static("public"));

app.post("/submit", (req, res) => {
  res.json({ received: req.body });
});

app.listen(3000);

Expected output for POST /submit with {"name": "Alice"}:

{"received": {"name": "Alice"}}

Common Mistakes

  1. Using app.use() for method-specific routesapp.use() matches all HTTP methods. Use app.get(), app.post(), etc. for method-specific handling.

  2. Placing error middleware before routes — Error-handling middleware must be registered after all routes to catch errors from them.

  3. Forgetting express.json() for POST bodies — Without express.json(), req.body is undefined for JSON payloads.

  4. Not calling next() with errors — Pass errors to next(err) rather than throwing them directly, or Express 4 will crash.

  5. Modifying req or res after sending response — Once res.send() is called, further modifications cause errors.

Practice Questions

  1. What is the difference between app.use() and app.get()? app.use() matches all HTTP methods for a given path. app.get() matches only GET requests.

  2. How does Express distinguish error-handling middleware from regular middleware? Error-handling middleware has exactly four parameters: err, req, res, next.

  3. When would you use router-level middleware instead of application-level? When specific routes (like admin endpoints) need special handling that other routes do not need.

  4. Challenge: Create a router for admin endpoints that checks for an admin token before allowing access.

const router = express.Router();
router.use((req, res, next) => {
  if (req.headers["x-admin-token"] !== "admin-secret") {
    return res.status(403).json({ error: "Admin access required" });
  }
  next();
});
router.get("/dashboard", (req, res) => res.json({ users: 150 }));
module.exports = router;

FAQ

Can I use multiple middleware functions on one route?

Yes. Pass them as separate arguments: app.get('/path', mw1, mw2, handler). They execute in order.

What happens if middleware calls next('route')?

It skips to the next matching route handler, bypassing any remaining middleware on the current route.

Does Express 5 handle middleware differently?

Express 5 natively handles async middleware errors without needing try/catch or a wrapper function.

Can middleware be applied conditionally?

Yes. Write middleware that checks a condition and calls next() or sends a response based on the result.

How do I test middleware in isolation?

Export the middleware function and pass mock req, res, and next objects in unit tests.

Mini Project

Build an Express API with three levels of middleware: application-level logging, router-level authentication for admin routes, and a global error handler.

const express = require("express");
const app = express();

app.use(express.json());
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();
});

const adminRouter = express.Router();
adminRouter.use((req, res, next) => {
  if (req.headers.authorization !== "Bearer admin-token") {
    return res.status(401).json({ error: "Unauthorized" });
  }
  next();
});
adminRouter.get("/users", (req, res) => {
  res.json([{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]);
});
app.use("/admin", adminRouter);

app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

app.listen(3000);

What's Next

Now that you understand Express middleware types, explore how to build logging middleware for production applications. Then learn about implementing authentication as middleware in Express.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro