If you’re learning Node.js and Express.js, you will probably come across the word middleware very quickly.
You may see code like this:
app.use(express.json());
or:
app.use(authMiddleware);
At first, middleware can seem confusing.
But the basic idea is actually very simple:
Middleware is a function that runs between receiving a request and sending a response.
Middleware can inspect a request, modify it, perform some logic, authenticate a user, handle errors, or pass the request to the next function.
In this guide, we’ll understand middleware from the basics with practical Node.js examples.
What Is Middleware?
In a Node.js application using Express, middleware is a function that has access to:
- The request object (
req) - The response object (
res) - The
next()function
A basic middleware looks like this:
const myMiddleware = (req, res, next) => {
console.log("Middleware executed");
next();
};
Then you can use it in your application:
app.use(myMiddleware);
When a request comes in:
Client
↓
Middleware
↓
Route
↓
Controller
↓
Response
The middleware gets an opportunity to do something before the request reaches the route handler.
Why Do We Need Middleware?
Imagine you have an application with 50 API routes.
You want to check whether a user is logged in before accessing certain routes.
Without middleware, you might have to write authentication logic inside every route:
app.get("/profile", (req, res) => {
// Check authentication
// Get profile
});
app.get("/orders", (req, res) => {
// Check authentication
// Get orders
});
app.get("/settings", (req, res) => {
// Check authentication
// Get settings
});
This creates duplicate code.
Instead, you can create one authentication middleware:
const authMiddleware = (req, res, next) => {
// Check authentication
next();
};
Then reuse it:
app.get("/profile", authMiddleware, getProfile);
app.get("/orders", authMiddleware, getOrders);
app.get("/settings", authMiddleware, getSettings);
Now authentication logic is centralized and reusable.
How Does Middleware Work?
The most important part of middleware is:
next();
When middleware finishes its work, it calls next() to tell Express:
“I’m done. Continue to the next middleware or route.”
For example:
const logger = (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
};
The flow becomes:
Request
↓
Logger Middleware
↓
next()
↓
Route Handler
↓
Response
If you forget to call next() and don’t send a response, the request can remain hanging.
A Simple Middleware Example
Let’s create a simple logger middleware.
const logger = (req, res, next) => {
console.log("Request received");
next();
};
Use it:
app.use(logger);
Now every request will pass through the middleware.
For example:
GET /users
The console might show:
Request received
Then Express continues to the /users route.
Middleware Parameters
Express middleware commonly looks like this:
(req, res, next) => {
}
Let’s understand each parameter.
req
req represents the incoming request.
It contains information such as:
req.body
req.params
req.query
req.headers
req.user
For example:
console.log(req.body);
res
res represents the response that will be sent back to the client.
For example:
res.json({
message: "Hello World"
});
next
next() passes control to the next middleware or route handler.
next();
This is what allows multiple middleware functions to work together.
Middleware Chain
One of the most powerful features of middleware is that you can chain multiple middleware functions.
For example:
app.get(
"/profile",
logger,
authMiddleware,
validateUser,
getProfile
);
The request flows like this:
Request
↓
Logger
↓
Authentication
↓
Validation
↓
Controller
↓
Response
Each middleware can perform a specific responsibility.
This makes your application easier to organize.
Types of Middleware in Express
There are several common types of middleware.
1. Application-Level Middleware
Application-level middleware is attached to the entire Express application using app.use().
Example:
app.use((req, res, next) => {
console.log("Application middleware");
next();
});
This middleware can run for every matching request.
You can also limit it to a specific path:
app.use("/api", (req, res, next) => {
console.log("API middleware");
next();
});
Now it runs for requests beginning with /api.
2. Router-Level Middleware
Router-level middleware works similarly to application-level middleware, but it is attached to an Express router.
Example:
const router = express.Router();
router.use((req, res, next) => {
console.log("Router middleware");
next();
});
You can then define routes:
router.get("/users", getUsers);
router.get("/orders", getOrders);
This is useful when you want middleware to apply only to a specific group of routes.
For example:
/api/users
/api/orders
/api/products
could have API-specific middleware.
3. Built-in Middleware
Express also provides built-in middleware.
One of the most commonly used examples is:
app.use(express.json());
This middleware parses incoming JSON request bodies.
Suppose the client sends:
{
"name": "John",
"email": "john@example.com"
}
With:
app.use(express.json());
you can access the data using:
req.body
For example:
app.post("/users", (req, res) => {
console.log(req.body);
res.json({
message: "User received"
});
});
4. Third-Party Middleware
You can also install middleware created by other developers.
A popular example is cors.
You might use:
import cors from "cors";
app.use(cors());
Another commonly used middleware is helmet, which helps set security-related HTTP headers.
Third-party middleware can save you from implementing common functionality yourself.
5. Error-Handling Middleware
Express has a special type of middleware for handling errors.
It has four parameters:
(err, req, res, next)
Example:
const errorHandler = (err, req, res, next) => {
console.error(err);
res.status(500).json({
message: "Something went wrong"
});
};
You can register it near the end of your middleware configuration:
app.use(errorHandler);
The important difference is the first err parameter.
(err, req, res, next)
This tells Express that the function is an error-handling middleware.
Authentication Middleware
One of the most common real-world uses of middleware is authentication.
Suppose your API uses JWT.
You might create:
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({
message: "Authentication required"
});
}
// Verify token...
next();
};
Then protect routes:
app.get(
"/profile",
authMiddleware,
getProfile
);
Now the request must pass authentication before reaching getProfile.
The flow becomes:
Client
↓
JWT Token
↓
Auth Middleware
↓
Token Valid?
├── No → 401
│
└── Yes
↓
Controller
↓
Response
Authorization Middleware
Authentication and authorization are different.
Authentication asks:
“Who are you?”
Authorization asks:
“What are you allowed to do?”
Middleware can also handle authorization.
For example:
const adminOnly = (req, res, next) => {
if (req.user.role !== "admin") {
return res.status(403).json({
message: "Access denied"
});
}
next();
};
Then:
app.delete(
"/users/:id",
authMiddleware,
adminOnly,
deleteUser
);
The flow becomes:
Request
↓
Authentication
↓
Authorization
↓
Controller
Validation Middleware
Middleware is also useful for validating incoming data.
For example:
const validateUser = (req, res, next) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({
message: "Name and email are required"
});
}
next();
};
Use it:
app.post(
"/users",
validateUser,
createUser
);
Now invalid data is rejected before reaching the controller.
Request Logging Middleware
Logging is another common use case.
You can create:
const logger = (req, res, next) => {
console.log(
`${req.method} ${req.originalUrl}`
);
next();
};
Then:
app.use(logger);
A request like:
GET /api/products
might produce:
GET /api/products
in your server logs.
For production applications, developers often use dedicated logging libraries instead of building a complete logging system themselves.
Modifying the Request Object
Middleware can also add information to the request object.
For example:
const authMiddleware = (req, res, next) => {
const user = {
id: "123",
role: "admin"
};
req.user = user;
next();
};
Then the controller can access:
req.user
For example:
const getProfile = (req, res) => {
console.log(req.user);
res.json({
user: req.user
});
};
This pattern is extremely common in authentication systems.
Middleware Order Matters
One important thing beginners should understand is that middleware runs in the order it is registered.
For example:
app.use(logger);
app.use(authMiddleware);
app.use(express.json());
These middleware functions don’t all execute randomly.
Express processes them according to the request flow and registration order.
For example:
Request
↓
Logger
↓
Authentication
↓
JSON Parser
↓
Route
Because of this, middleware order can affect application behavior.
Route-Specific Middleware
You don’t always need middleware globally.
You can apply middleware to only one route:
app.get(
"/admin",
authMiddleware,
adminOnly,
getAdminDashboard
);
Other routes don’t have to use adminOnly.
This is useful for protected resources.
Multiple Middleware Functions
You can use multiple middleware functions on one route.
app.post(
"/users",
authMiddleware,
validateUser,
createUser
);
The request flows through:
Authentication
↓
Validation
↓
Create User
If authentication fails:
Authentication
↓
401 Response
The request doesn’t continue.
If validation fails:
Authentication
↓
Validation
↓
400 Response
Again, the controller isn’t executed.
Middleware vs Controller
Beginners sometimes confuse middleware with controllers.
They have different responsibilities.
Middleware
Middleware usually handles cross-cutting logic such as:
- Authentication
- Authorization
- Validation
- Logging
- Parsing
- Rate limiting
Controller
A controller usually handles the actual business operation.
For example:
const createUser = async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
};
A clean architecture might look like:
Request
↓
Middleware
↓
Validation
↓
Authentication
↓
Controller
↓
Service
↓
Database
This separation makes the application easier to maintain.
Middleware in a Real Node.js Project
A project might have a structure like:
src/
├── controllers/
│ └── user.controller.ts
│
├── middleware/
│ ├── auth.middleware.ts
│ ├── error.middleware.ts
│ └── validation.middleware.ts
│
├── routes/
│ └── user.routes.ts
│
├── services/
│ └── user.service.ts
│
└── app.ts
Then your route could look like:
router.post(
"/users",
authMiddleware,
validateUser,
createUser
);
This is a common way to keep responsibilities separated in larger Node.js applications.
Common Middleware Mistakes
Forgetting next()
This is one of the most common mistakes.
const middleware = (req, res, next) => {
console.log("Hello");
// Forgot next()
};
If the middleware doesn’t send a response and doesn’t call next(), the request can get stuck.
Calling next() After Sending a Response
Avoid doing this:
if (!user) {
res.status(401).json({
message: "Unauthorized"
});
next();
}
Instead, return immediately:
if (!user) {
return res.status(401).json({
message: "Unauthorized"
});
}
Otherwise, another handler may execute after the response has already been sent.
Middleware in One Simple Example
Here’s a complete small example:
import express from "express";
const app = express();
app.use(express.json());
const logger = (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
};
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({
message: "Unauthorized"
});
}
next();
};
app.get(
"/profile",
logger,
authMiddleware,
(req, res) => {
res.json({
message: "Profile data"
});
}
);
app.listen(3000, () => {
console.log("Server running on port 3000");
});
The request flow is:
Request
↓
express.json()
↓
logger
↓
authMiddleware
↓
Profile Route
↓
Response
Why Middleware Is Important
Middleware helps developers build applications using small, reusable pieces of logic.
Instead of putting everything into one huge route handler:
Authentication
Validation
Logging
Authorization
Business Logic
Database
Response
you can separate responsibilities:
Request
↓
Logger Middleware
↓
Auth Middleware
↓
Validation Middleware
↓
Authorization Middleware
↓
Controller
↓
Service
↓
Database
This makes your application:
- Easier to understand
- Easier to test
- Easier to maintain
- Easier to reuse
- Easier to scale
Final Takeaway
If you’re new to Node.js, remember this simple definition:
Middleware is a function that sits in the request-response cycle and can inspect, modify, reject, or pass a request to the next handler.
The most important concept to understand is:
next();
When middleware calls next(), Express continues processing the request.
You can use middleware for:
- Authentication
- Authorization
- Validation
- Logging
- Error handling
- Request parsing
- Rate limiting
- Adding data to requests
Once you understand middleware, concepts such as authentication middleware, authorization, Express routing, JWT authentication, and API security become much easier to understand.
In simple terms:
Middleware is the checkpoint between a request and the code that handles that request.




