Skip to main content

Command Palette

Search for a command to run...

What is Middleware in Express and How It Works

Updated
15 min read
What is Middleware in Express and How It Works
H
CS undergrad | Tech enthusiast | Focusing on Web Dev • DSA • ML | Building skills for real-world impact

Middleware is code that runs between receiving a request and sending a response. It's like a security checkpoint at an airport: every passenger passes through before boarding. Your request passes through middleware before reaching the route handler.

This is about understanding middleware and building better Express applications.


What is Middleware in Express?

Middleware is a function that processes requests and responses.

Simple Definition

Middleware is a function that:

  1. Receives the request object
  2. Does something with it
  3. Either sends a response or passes control to the next middleware

Real Example

When you visit a website:

User makes request
      |
      v
Middleware 1 checks if logged in
      |
      v
Middleware 2 validates the request
      |
      v
Middleware 3 logs the request
      |
      v
Route handler processes it
      |
      v
Response is sent

The request goes through several checkpoints before the actual route handler.

The Checkpoint Analogy

Think of middleware like checkpoints at an airport:

Passenger enters airport
      |
      v
Checkpoint 1: Check ticket
      |
      v
Checkpoint 2: Security scan
      |
      v
Checkpoint 3: Passport check
      |
      v
Board the plane

Each checkpoint can:

  • Allow you through (call next())
  • Stop you (send a response)
  • Do something as you pass (check ticket, scan bag)

Express middleware works the same way.

Basic Middleware Function

function myMiddleware(req, res, next) {
  // req = incoming request
  // res = response to send
  // next = function to call to move to next middleware
  
  console.log("Request received");
  next();  // Pass to next middleware
}

app.use(myMiddleware);

All middleware functions have this signature: (req, res, next).


Where Middleware Sits in Request Lifecycle

The Complete Request Flow

Client sends HTTP request
      |
      v
Request arrives at Express
      |
      v
Middleware 1 runs
      |
      v
Middleware 2 runs
      |
      v
Middleware 3 runs
      |
      v
Route handler runs
      |
      v
Response is created
      |
      v
Response sent to client
      |
      v
Client receives response

Middleware runs before the route handler. The route handler is the final step.

Request Pipeline

Imagine a factory assembly line:

Raw materials (request)
      |
      v
Station 1: Clean (middleware 1)
      |
      v
Station 2: Paint (middleware 2)
      |
      v
Station 3: Inspect (middleware 3)
      |
      v
Station 4: Package (route handler)
      |
      v
Finished product (response)

Each station modifies or inspects. The final station packages everything.

Code Example

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

// Middleware 1
app.use((req, res, next) => {
  console.log("Middleware 1: Request started at", new Date());
  next();  // Move to next middleware
});

// Middleware 2
app.use((req, res, next) => {
  console.log("Middleware 2: Processing request");
  next();  // Move to next middleware
});

// Route handler
app.get("/", (req, res) => {
  console.log("Route handler: Sending response");
  res.send("Hello World");
});

app.listen(3000);

Order of execution:

Request to /
      |
      v
Middleware 1 runs: "Request started..."
      |
      v
Middleware 2 runs: "Processing request"
      |
      v
Route handler runs: "Sending response"
      |
      v
Response sent: "Hello World"

Types of Middleware

Type 1: Application-Level Middleware

Runs for every request to your application.

// Applies to every request
app.use((req, res, next) => {
  console.log("This runs for every request");
  next();
});

Type 2: Router-Level Middleware

Runs only for specific routes.

// Applies only to /api routes
app.use("/api", (req, res, next) => {
  console.log("This only runs for /api routes");
  next();
});

Type 3: Built-in Middleware

Express provides middleware for common tasks.

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

// Parse form data
app.use(express.urlencoded({ extended: true }));

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

Type 4: Error-Handling Middleware

Catches errors from other middleware.

// Must have 4 parameters (err, req, res, next)
app.use((err, req, res, next) => {
  console.log("Error occurred:", err.message);
  res.status(500).json({ error: "Something went wrong" });
});

Error handlers must be defined last.


Application-Level Middleware

Runs for all requests.

Basic Application Middleware

app.use((req, res, next) => {
  console.log("Running for every request");
  next();
});

app.get("/", (req, res) => {
  res.send("Home");
});

app.get("/about", (req, res) => {
  res.send("About");
});

Both GET requests pass through the middleware.

Multiple Application Middleware

// Middleware 1
app.use((req, res, next) => {
  console.log("Middleware 1");
  next();
});

// Middleware 2
app.use((req, res, next) => {
  console.log("Middleware 2");
  next();
});

// Middleware 3
app.use((req, res, next) => {
  console.log("Middleware 3");
  next();
});

app.get("/", (req, res) => {
  console.log("Route handler");
  res.send("Done");
});

Execution order:

Middleware 1
Middleware 2
Middleware 3
Route handler

Conditional Application Middleware

// Only for GET requests
app.use((req, res, next) => {
  if (req.method === "GET") {
    console.log("This is a GET request");
  }
  next();
});

// Only for specific paths
app.use((req, res, next) => {
  if (req.path.startsWith("/admin")) {
    console.log("Admin route accessed");
  }
  next();
});

Router-Level Middleware

Runs only for specific routes.

Basic Router Middleware

// Applies only to /admin routes
app.use("/admin", (req, res, next) => {
  console.log("Admin route accessed");
  next();
});

app.get("/admin/dashboard", (req, res) => {
  res.send("Admin Dashboard");
});

app.get("/admin/users", (req, res) => {
  res.send("User List");
});

// This route doesn't go through the middleware
app.get("/", (req, res) => {
  res.send("Home");
});

The middleware only runs for requests starting with /admin.

Multiple Router Middleware

// First middleware
app.use("/api", (req, res, next) => {
  console.log("API request received");
  next();
});

// Second middleware
app.use("/api", (req, res, next) => {
  console.log("Processing API request");
  next();
});

app.get("/api/users", (req, res) => {
  res.send("Users");
});

Both middlewares run for /api/users in order.

Nested Routes with Middleware

// Auth middleware only for protected routes
app.use("/api/admin", (req, res, next) => {
  console.log("Checking authentication");
  next();
});

app.get("/api/admin/stats", (req, res) => {
  res.send("Admin stats");
});

app.get("/api/public/posts", (req, res) => {
  res.send("Public posts");
});

/api/admin/stats passes through middleware. /api/public/posts doesn't.


Built-in Middleware

Express provides common middleware.

JSON Parser

app.use(express.json());

app.post("/data", (req, res) => {
  console.log(req.body);  // Parsed JSON available
  res.send("Data received");
});

Automatically parses JSON request bodies.

Form Data Parser

app.use(express.urlencoded({ extended: true }));

app.post("/form", (req, res) => {
  console.log(req.body);  // Form data available
  res.send("Form received");
});

Parses form submissions.

Static File Serving

app.use(express.static("public"));

// Now http://localhost:3000/style.css serves public/style.css
// And http://localhost:3000/index.html serves public/index.html

Serves files from a directory.

Static with Path Prefix

app.use("/static", express.static("public"));

// Now http://localhost:3000/static/style.css serves public/style.css

Serve static files under a specific path.


The Role of next()

next() is crucial. It tells Express to move to the next middleware.

Without next()

app.use((req, res, next) => {
  console.log("Middleware running");
  // Forgot to call next()
});

app.get("/", (req, res) => {
  res.send("Home");  // This never runs
});

The request hangs. The route handler never runs.

With next()

app.use((req, res, next) => {
  console.log("Middleware running");
  next();  // Pass to next middleware
});

app.get("/", (req, res) => {
  res.send("Home");  // Now this runs
});

The request moves forward. The route handler runs.

Sending Response Instead of next()

app.use((req, res, next) => {
  if (!isAuthenticated(req)) {
    res.status(401).json({ error: "Not authenticated" });
    return;  // Stop here, don't call next()
  }
  next();  // Continue if authenticated
});

app.get("/protected", (req, res) => {
  res.send("Protected content");
});

If authentication fails, send response. Otherwise, continue.

next() with Errors

app.use((req, res, next) => {
  try {
    // Some operation
    JSON.parse("invalid json");
    next();
  } catch (error) {
    next(error);  // Pass error to error handler
  }
});

// Error handler (4 parameters)
app.use((err, req, res, next) => {
  console.log("Error:", err.message);
  res.status(500).send("Error occurred");
});

Pass errors to error handlers with next(error).


Middleware Execution Order

Middleware runs in the order it's defined.

Definition Order Matters

// This runs first
app.use((req, res, next) => {
  console.log("First");
  next();
});

// This runs second
app.use((req, res, next) => {
  console.log("Second");
  next();
});

// This runs third
app.use((req, res, next) => {
  console.log("Third");
  next();
});

app.get("/", (req, res) => {
  console.log("Route handler");
  res.send("Done");
});

Output:

First
Second
Third
Route handler

Changing Order

app.use((req, res, next) => {
  console.log("A");
  next();
});

app.get("/", (req, res) => {
  res.send("Home");
});

app.use((req, res, next) => {
  console.log("B");  // This is defined AFTER the route
  next();
});

Output for GET /:

A
(No B - defined after route)

Middleware defined after a route won't run for that route.

Route-Specific Order

// Runs for all /admin routes
app.use("/admin", (req, res, next) => {
  console.log("Admin middleware");
  next();
});

app.get("/admin/users", (req, res) => {
  res.send("Users");
});

// Global middleware defined after still runs first
app.use((req, res, next) => {
  console.log("Global middleware");
  next();
});

Global middleware defined before route-specific middleware runs first.


Request → Middleware → Route Handler Flow

HTTP Request arrives
      |
      v
Express receives request
      |
      v
Global middleware 1 executes
  (can access req, res)
  (can modify request)
  |
  ├─ Call next() → continue
  └─ Send response → stop here
      |
      v
Global middleware 2 executes
      |
      ├─ Call next() → continue
      └─ Send response → stop here
      |
      v
Route-specific middleware executes
      |
      ├─ Call next() → continue
      └─ Send response → stop here
      |
      v
Route handler executes
      |
      ├─ Send response → stop here
      └─ Call next() → (if defined)
      |
      v
Response sent to client
      |
      v
Client receives response

Real-World Example: Logging Middleware

Log every request.

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

// Logging middleware
app.use((req, res, next) => {
  const timestamp = new Date().toISOString();
  console.log(`[\({timestamp}] \){req.method} ${req.path}`);
  next();
});

app.get("/", (req, res) => {
  res.send("Home");
});

app.get("/about", (req, res) => {
  res.send("About");
});

app.listen(3000, () => {
  console.log("Server running");
});

Output when accessing routes:

[2026-05-04T10:30:45.123Z] GET /
[2026-05-04T10:30:50.456Z] GET /about

Enhanced Logging Middleware

app.use((req, res, next) => {
  const start = Date.now();
  
  // Log when request ends
  res.on("finish", () => {
    const duration = Date.now() - start;
    console.log(
      `\({req.method} \){req.path} - \({res.statusCode} - \){duration}ms`
    );
  });
  
  next();
});

Logs request duration too.


Real-World Example: Authentication Middleware

Protect routes from unauthorized access.

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

// Authentication middleware
function authMiddleware(req, res, next) {
  const token = req.headers.authorization;
  
  if (!token) {
    return res.status(401).json({ error: "No token provided" });
  }
  
  if (token === "Bearer valid-token-123") {
    req.user = { id: 1, name: "Alice" };
    next();
  } else {
    res.status(401).json({ error: "Invalid token" });
  }
}

// Public route
app.get("/", (req, res) => {
  res.send("Welcome");
});

// Protected route (use middleware)
app.get("/profile", authMiddleware, (req, res) => {
  res.send(`Hello ${req.user.name}`);
});

app.listen(3000);

Test without token:

curl http://localhost:3000/profile
# Error: No token provided

Test with token:

curl -H "Authorization: Bearer valid-token-123" http://localhost:3000/profile
# Hello Alice

Protecting Multiple Routes

// Apply middleware to all /api routes
app.use("/api", authMiddleware);

app.get("/api/users", (req, res) => {
  res.send("Users list");
});

app.get("/api/posts", (req, res) => {
  res.send("Posts list");
});

// This route is unprotected
app.get("/public", (req, res) => {
  res.send("Public data");
});

All /api routes require authentication. /public doesn't.


Real-World Example: Request Validation Middleware

Validate request data before processing.

const express = require("express");
const app = express();
app.use(express.json());

// Validation middleware
function validateEmail(req, res, next) {
  const email = req.body.email;
  
  if (!email) {
    return res.status(400).json({ error: "Email is required" });
  }
  
  if (!email.includes("@")) {
    return res.status(400).json({ error: "Invalid email format" });
  }
  
  next();
}

app.post("/signup", validateEmail, (req, res) => {
  const email = req.body.email;
  res.json({ message: `Signed up with ${email}` });
});

app.listen(3000);

Test with valid email:

curl -X POST http://localhost:3000/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com"}'
# Signed up with alice@example.com

Test with invalid email:

curl -X POST http://localhost:3000/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"invalid"}'
# Invalid email format

Test without email:

curl -X POST http://localhost:3000/signup \
  -H "Content-Type: application/json" \
  -d '{}'
# Email is required

Multiple Validations

function validateName(req, res, next) {
  if (!req.body.name || req.body.name.length < 2) {
    return res.status(400).json({ error: "Name must be at least 2 characters" });
  }
  next();
}

function validateEmail(req, res, next) {
  if (!req.body.email || !req.body.email.includes("@")) {
    return res.status(400).json({ error: "Invalid email" });
  }
  next();
}

app.post("/signup", validateName, validateEmail, (req, res) => {
  res.json({ message: "Signup successful" });
});

Both validations run before the route handler.


Multiple Middleware Execution Chain

Request arrives
      |
      v
Middleware 1: Logging
  console.log("Request started")
  |
  ├─ next() called
  |
  v
Middleware 2: Authentication
  Check token
  |
  ├─ Token valid: next()
  ├─ Token invalid: send 401 error (stop)
  |
  v
Middleware 3: Request Validation
  Validate data
  |
  ├─ Data valid: next()
  ├─ Data invalid: send 400 error (stop)
  |
  v
Middleware 4: Add Timestamp
  req.timestamp = Date.now()
  |
  ├─ next() called
  |
  v
Route Handler
  Process request
  Send response
  |
  v
Response sent to client

Each middleware runs in order. If any middleware sends a response, the chain stops.


Complete Practical Example

const express = require("express");
const app = express();
app.use(express.json());

// Middleware 1: Logging
app.use((req, res, next) => {
  const timestamp = new Date().toISOString();
  console.log(`[\({timestamp}] \){req.method} ${req.path}`);
  next();
});

// Middleware 2: Authentication
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
  
  if (!token) {
    return res.status(401).json({ error: "No token" });
  }
  
  if (token === "valid-token") {
    req.user = { id: 1, name: "Alice" };
    next();
  } else {
    res.status(401).json({ error: "Invalid token" });
  }
}

// Middleware 3: Request validation
function validateRequest(req, res, next) {
  if (!req.body.email) {
    return res.status(400).json({ error: "Email required" });
  }
  
  if (!req.body.email.includes("@")) {
    return res.status(400).json({ error: "Invalid email" });
  }
  
  next();
}

// Public route (only logging)
app.get("/", (req, res) => {
  res.send("Home");
});

// Protected route (logging + authentication)
app.get("/profile", authenticate, (req, res) => {
  res.json({ message: `Hello ${req.user.name}` });
});

// Protected with validation (logging + authentication + validation)
app.post("/update-email", authenticate, validateRequest, (req, res) => {
  res.json({ message: `Email updated to ${req.body.email}` });
});

// Error handler
app.use((err, req, res, next) => {
  console.log("Error:", err.message);
  res.status(500).json({ error: "Server error" });
});

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

Test:

# Public route (no authentication needed)
curl http://localhost:3000/
# Home

# Protected route without token
curl http://localhost:3000/profile
# No token

# Protected route with token
curl -H "Authorization: Bearer valid-token" http://localhost:3000/profile
# Hello Alice

# Update email without token
curl -X POST http://localhost:3000/update-email \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com"}'
# No token

# Update email with token
curl -X POST http://localhost:3000/update-email \
  -H "Authorization: Bearer valid-token" \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com"}'
# Email updated to alice@example.com

Practice Assignment

1. Create a logging middleware:

// Log every request with timestamp, method, and path
// Must call next() to continue
// Apply to all routes

2. Build an authentication middleware:

// Check Authorization header for token
// If no token, send 401 error
// If valid token, set req.user and call next()
// Protect one route with it

3. Create request validation middleware:

// Validate req.body.name (required, min 2 chars)
// Validate req.body.email (required, contains @)
// Return 400 error if invalid
// Use on a POST route

4. Chain multiple middleware:

// Create logging → authentication → validation chain
// POST route should require all three
// GET route should only require logging
// Test both routes

5. Create route-specific middleware:

// Create /api/admin routes protected by authentication
// Create /api/public routes without protection
// Show how same app has protected and public routes

Quick Recap

  • Middleware is code that runs between receiving a request and sending a response.

  • Middleware sits in the request pipeline: request → middleware → handler → response.

  • Every middleware function takes three parameters: req, res, and next.

  • next() passes control to the next middleware in the chain.

  • If middleware sends a response, the chain stops (don't call next()).

  • Application-level middleware runs for all requests using app.use().

  • Router-level middleware runs only for specific paths using app.use("/path", ...).

  • Built-in middleware includes express.json(), express.urlencoded(), and express.static().

  • Error-handling middleware has four parameters: (err, req, res, next).

  • Middleware runs in the order it's defined.

  • Middleware defined before a route affects that route.

  • Middleware defined after a route doesn't affect that route.

  • Logging middleware tracks all incoming requests.

  • Authentication middleware protects routes from unauthorized access.

  • Validation middleware checks request data before processing.

  • Multiple middleware can be applied to a single route.

  • Middleware can modify the request object (add properties like req.user).

  • Use conditional logic in middleware to run code based on request properties.

Middleware is the foundation of building scalable Express applications.


If you enjoyed this article, check out my other blogs on this profile. Connect with me: LinkedIn | GitHub | X (Twitter)