Skip to main content

Skillber v1.0 is here!

Learn more

Middleware

Checking access...

Middleware functions are the building blocks of Express applications. They have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle.

What is Middleware?

Each middleware function can:

  1. Execute any code
  2. Modify the req and res objects
  3. End the request-response cycle
  4. Call the next middleware in the stack
const logger = (req, res, next) => {
console.log(`${req.method} ${req.path}${new Date().toISOString()}`);
next(); // pass control to the next middleware
};
app.use(logger); // applies to all routes

Middleware Flow

Request → Middleware 1 → Middleware 2 → Route Handler → Response
↓ ↓
next() next()

If a middleware doesn’t call next() and doesn’t send a response, the request hangs.

Built-in Middleware

express.json()

Parses incoming JSON request bodies:

app.use(express.json());
app.post("/api/users", (req, res) => {
console.log(req.body); // parsed JSON object
res.json({ received: req.body });
});

express.urlencoded()

Parses form data from application/x-www-form-urlencoded:

app.use(express.urlencoded({ extended: true }));
// extended: true allows rich objects (not just strings)

express.static()

Serves static files from a directory:

// Serve files from the "public" folder
app.use(express.static("public"));
// Now http://localhost:3000/image.jpg serves public/image.jpg
// With virtual path prefix
app.use("/static", express.static("public"));
// http://localhost:3000/static/image.jpg
// Multiple directories (searched in order)
app.use(express.static("public"));
app.use(express.static("uploads"));

Third-Party Middleware

cors

Enable Cross-Origin Resource Sharing:

Terminal window
npm install cors
const cors = require("cors");
// Allow all origins (development)
app.use(cors());
// Specific configuration
app.use(cors({
origin: "https://myapp.com",
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 86400, // cache preflight for 24 hours
}));

morgan (HTTP request logger)

Terminal window
npm install morgan
const morgan = require("morgan");
// Predefined formats
app.use(morgan("dev")); // concise output colored by status
app.use(morgan("combined")); // Apache-style (more verbose)
app.use(morgan("tiny")); // minimal
// Custom format
app.use(morgan(":method :url :status :response-time ms"));

helmet (Security headers)

Terminal window
npm install helmet
const helmet = require("helmet");
app.use(helmet()); // sets various HTTP security headers

Custom Middleware

Request Logger

const requestLogger = (req, res, next) => {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
console.log(
`${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`
);
});
next();
};
app.use(requestLogger);

Authentication Check

const requireAuth = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: "Authentication required" });
}
try {
const user = jwt.verify(token.replace("Bearer ", ""), process.env.JWT_SECRET);
req.user = user; // Attach user to request for downstream handlers
next();
} catch (err) {
return res.status(401).json({ error: "Invalid token" });
}
};
// Apply to specific routes
app.get("/api/profile", requireAuth, (req, res) => {
res.json({ user: req.user });
});

Rate Limiting

const rateLimit = {};
const rateLimiter = (req, res, next) => {
const ip = req.ip;
const now = Date.now();
const windowMs = 60 * 1000; // 1 minute
const maxRequests = 100;
if (!rateLimit[ip]) {
rateLimit[ip] = { count: 1, start: now };
return next();
}
if (now - rateLimit[ip].start > windowMs) {
rateLimit[ip] = { count: 1, start: now };
return next();
}
rateLimit[ip].count++;
if (rateLimit[ip].count > maxRequests) {
return res.status(429).json({ error: "Too many requests" });
}
next();
};
app.use(rateLimiter);

For production, use express-rate-limit package instead.

Route-Specific Middleware

// Middleware applied to specific route
const validateUserInput = (req, res, next) => {
const { name, email } = req.body;
if (!name || name.length < 2) {
return res.status(400).json({ error: "Name must be at least 2 characters" });
}
if (!email || !email.includes("@")) {
return res.status(400).json({ error: "Valid email is required" });
}
next();
};
app.post("/api/users", validateUserInput, createUser);

Multiple Middleware Functions

app.get(
"/api/protected",
requireAuth, // first: check auth
validateInput, // second: validate data
cacheCheck, // third: check cache
(req, res) => { // fourth: route handler
res.json({ data: "protected data" });
}
);

Error-Handling Middleware

Error-handling middleware has four parameters instead of three:

// This must be defined AFTER all routes and other middleware
app.use((err, req, res, next) => {
console.error("Error:", err);
res.status(err.status || 500).json({
error: {
message: err.message || "Internal server error",
...(process.env.NODE_ENV === "development" && { stack: err.stack }),
},
});
});

Custom Error Class

class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// Routes can throw operational errors:
app.get("/users/:id", async (req, res, next) => {
try {
const user = await findUser(req.params.id);
if (!user) {
throw new AppError("User not found", 404);
}
res.json(user);
} catch (err) {
next(err); // passes to error handler
}
});
// Global error handler
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
status: "error",
statusCode,
message: err.isOperational ? err.message : "Something went wrong",
});
});

Middleware Ordering

Order matters — middleware runs in the order it’s defined:

// 1. Security & logging (applied first)
app.use(helmet());
app.use(cors());
app.use(morgan("dev"));
// 2. Body parsers
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 3. Static files
app.use(express.static("public"));
// 4. Custom middleware
app.use(requestLogger);
// 5. Routes
app.use("/api/users", usersRouter);
app.use("/api/products", productsRouter);
// 6. 404 handler (must be after all routes)
app.use("*", (req, res) => {
res.status(404).json({ error: "Route not found" });
});
// 7. Error handler (must be last)
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: "Internal server error" });
});

Quick Reference

// Application-level
app.use(middleware); // all routes
app.use("/path", middleware); // specific path prefix
// Router-level
router.use(middleware);
// Route-level
app.get("/path", mw1, mw2, handler);
// Error-handling (4 params)
app.use((err, req, res, next) => {});
// Common middleware
express.json() // parse JSON bodies
express.urlencoded() // parse form bodies
express.static("dir") // serve static files
cors() // enable CORS
helmet() // security headers
morgan("dev") // request logging

Practice Exercises

  1. Request timer middleware: Create middleware that logs [METHOD] /path — Nms for every request using process.hrtime() for precise timing.

  2. API key authentication: Build middleware that checks for an x-api-key header. If the key matches a stored value (in env), allow the request. Otherwise, return 401. Protect specific routes with this middleware.

  3. Error handling system: Create a custom AppError class with status code and an isOperational flag. Build an error handler middleware that distinguishes operational errors (return the message) from programming errors (return generic message, log the full error).