Frontend
What Is Middleware and Why Do Backend Developers Use It?
Tanu Priya Dev.to (EN Zone)
9 views
When a request reaches a backend, it usually doesn't go directly from the client to the controller.
There are often several things the server needs to check or do first.
For example, before allowing a user to access:
GET /api/profile
the backend might need to:
Check whether the user is authenticated
Verify their permissions
Log the request
Validate input
Check rate limits
Handle unexpected errors
If every controller had to implement all of these responsibilities itself, backend code would quickly become repetitive and difficult to maintain.
This is where middleware comes in.
A simple way to think about middleware is:
Request
↓
Middleware
↓
Middleware
↓
Middleware
↓
Controller
↓
Response
Middleware sits in the request-processing pipeline and can inspect, modify, allow, reject, or pass along a request.
1. What Exactly Is Middleware?
Middleware is code that runs between receiving a request and producing the final response.
In Express, middleware commonly looks like:
function logger(req, res, next) {
console.log(req.method, req.url);
next();
}
The important part is:
next();
Calling next() tells Express:
"I'm done with my work. Continue processing this request."
So the flow becomes:
Request
↓
logger()
↓
next()
↓
Controller
↓
Response
If middleware doesn't call next() and doesn't send a response, the request can remain stuck.
Middleware can therefore do three major things:
1. Continue the request
2. Modify the request/response
3. Stop the request
For example:
function authenticate(req, res, next) {
if (!req.user) {
return res.status(401).json({
error: "Unauthorized"
});
}
next();
}
Here, an unauthenticated request never reaches the controller.
2. Why Not Put Everything Inside the Controller?
Suppose you have:
app.get("/profile", (req, res) => {
// Check authentication
// Check permissions
// Log request
// Validate something
// Fetch user
// Return response
});
Now imagine 30 different protected endpoints.
You might end up repeating:
Authentication
Authorization
Logging
Validation
Rate limiting
inside many controllers.
That creates duplicated code.
Middleware lets you move reusable responsibilities into separate functions:
Request
↓
Authentication
↓
Rate Limiting
↓
Validation
↓
Controller
Now the controller can focus on the actual business operation.
This is one of the biggest reasons backend developers use middleware:
Separate common request-processing concerns from business logic.
3. Authentication Middleware
One of the most common uses of middleware is authentication.
Suppose a user requests:
GET /api/profile
Authorization: Bearer <token>
The backend needs to determine whether the token is valid.
Instead of doing this inside every controller, you can create:
function authenticate(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({
error: "Authentication required"
});
}
// Verify token
req.user = decodedUser;
next();
}
Then:
app.get(
"/api/profile",
authenticate,
getProfile
);
The flow becomes:
Request
↓
Authentication Middleware
↓
Token Valid?
|
├── No → 401
|
└── Yes
↓
Controller
↓
Response
The controller doesn't need to worry about how authentication works.
It can simply use:
req.user
to know which user is making the request.
4. Authentication vs Authorization
These two concepts are often confused.
Authentication asks:
Who are you?
Authorization asks:
Are you allowed to do this?
For example:
User logs in
↓
Authentication
↓
User identified
↓
Authorization
↓
Check permissions
You might have:
app.delete(
"/api/users/:id",
authenticate,
requireAdmin,
deleteUser
);
The middleware chain becomes:
Request
↓
authenticate
↓
requireAdmin
↓
deleteUser
This makes the security requirements visible directly in the route.
5. Logging Middleware
Another common use is logging.
You might want to know:
Which endpoint was called?
Which HTTP method was used?
How long did it take?
What was the response status?
A simple middleware could be:
function logger(req, res, next) {
console.log(
req.method,
req.originalUrl
);
next();
}
Then:
app.use(logger);
Now every request passes through it.
The flow becomes:
GET /api/products
↓
Logger
↓
Router
↓
Controller
↓
Response
In production systems, logging middleware can be much more sophisticated.
It might capture:
Request ID
Timestamp
HTTP method
Path
Status code
Response time
User ID
Server instance
This information becomes extremely useful when debugging production problems.
6. Why Request IDs Matter
Imagine a request travels through several services:
Client
↓
API Gateway
↓
User Service
↓
Payment Service
↓
Database
A single user action might generate many logs.
Without a shared request identifier, connecting those logs can be difficult.
Middleware can generate or propagate a request ID:
Request
↓
Request ID Middleware
↓
Authentication
↓
Router
↓
Controller
Then different parts of the system can log:
request_id = abc123
This allows developers to trace one request across multiple components.
This becomes particularly valuable in distributed systems.
7. Validation Middleware
Clients can send unexpected or invalid data.
Suppose an API expects:
{
"name": "Alex",
"age": 22
}
But the client sends:
{
"name": "",
"age": "hello"
}
The backend should validate the input before executing business logic.
You could create:
function validateUser(req, res, next) {
const { name, age } = req.body;
if (!name || typeof age !== "number") {
return res.status(400).json({
error: "Invalid input"
});
}
next();
}
Then:
app.post(
"/api/users",
validateUser,
createUser
);
The flow becomes:
Request
↓
Validation
↓
Valid?
|
├── No → 400
|
└── Yes
↓
Controller
This keeps invalid requests away from the business logic.
8. Validation Is Not Only About Types
Validation can involve much more than checking whether something is a string or number.
For example:
Email format
Password length
Required fields
Allowed values
Maximum length
Date format
Pagination limits
File size
For example:
POST /api/products
might require:
name → required
price → positive number
category → allowed value
description → maximum length
A validation middleware can reject the request before it reaches the product service.
This reduces unnecessary work and makes API behavior more predictable.
9. Rate Limiting Middleware
Imagine someone sends:
10,000 requests
↓
within a few seconds
to your login endpoint.
That can overload the system or facilitate abuse.
Rate limiting middleware can control how many requests a client is allowed to make within a period.
For example:
100 requests
↓
per minute
↓
per IP / user / API key
Conceptually:
Request
↓
Rate Limiter
↓
Limit exceeded?
|
├── Yes → 429 Too Many Requests
|
└── No
↓
Router
The standard HTTP response for rate limiting is:
429 Too Many Requests
Rate limiting is particularly useful for endpoints such as:
/login
/signup
/password-reset
/search
/public APIs
because these endpoints can be attractive targets for abuse.
10. Rate Limiting Usually Needs Shared State
There's an important system-design detail here.
Suppose you have three backend servers:
Load Balancer
↓
┌─────────┼─────────┐
↓ ↓ ↓
Server 1 Server 2 Server 3
If each server keeps its own rate-limit counter in memory, a client might effectively get a separate limit on each server.
For example:
Server 1 → 100 requests
Server 2 → 100 requests
Server 3 → 100 requests
Now the intended limit may not behave as expected.
A distributed rate limiter often uses shared storage such as Redis:
Servers
↓
Shared Rate Limit Store
↓
Redis
This is a good example of how a seemingly simple middleware feature becomes a system-design problem when the application scales.
11. Error Handling Middleware
Not every request succeeds.
A database might fail.
An external API might timeout.
A programmer might accidentally throw an exception.
Instead of handling every error differently inside every route, applications can centralize error handling.
Conceptually:
Request
↓
Middleware
↓
Controller
↓
Service
↓
Error
↓
Error Middleware
↓
Response
In Express, an error-handling middleware has a special signature:
function errorHandler(err, req, res, next) {
console.error(err);
res.status(500).json({
error: "Internal server error"
});
}
This provides one place to control how errors are logged and returned to clients.
12. Why Centralized Error Handling Helps
Imagine 50 different endpoints.
Without centralized error handling, you might end up with different responses:
{
"message": "Something failed"
}
Another endpoint might return:
{
"error": "Database error"
}
Another might return:
{
"success": false
}
This makes the API harder to consume.
A centralized error layer can provide a consistent structure.
For example:
{
"error": {
"code": "INTERNAL_ERROR",
"message": "Something went wrong"
}
}
The frontend now knows what kind of response to expect.
13. Middleware Can Modify Requests
Middleware doesn't only validate or reject requests.
It can also attach useful information to the request.
For example:
function authenticate(req, res, next) {
const user = verifyToken(
req.headers.authorization
);
req.user = user;
next();
}
Later:
function getProfile(req, res) {
const userId = req.user.id;
// Fetch profile
}
The authentication middleware enriched the request with user information.
The controller can then use that information without repeating the authentication logic.
This pattern is common throughout backend applications.
14. Middleware Can Be Global or Specific
Middleware can be applied to every request:
app.use(logger);
Or only to specific routes:
app.get(
"/api/profile",
authenticate,
getProfile
);
Or to an entire group of routes:
app.use(
"/api/admin",
authenticate,
requireAdmin,
adminRoutes
);
This gives developers control over where a middleware should run.
You don't want authentication middleware on a public endpoint such as:
GET /api/products
if that endpoint is intentionally public.
But you probably do want it on:
GET /api/profile
15. Middleware Chains
The real power comes from combining middleware.
Consider:
app.post(
"/api/orders",
authenticate,
rateLimit,
validateOrder,
createOrder
);
The request travels through:
POST /api/orders
↓
Authentication
↓
Rate Limiting
↓
Validation
↓
Controller
↓
Order Service
↓
Database
↓
Response
Each layer has one primary responsibility.
Authentication doesn't need to validate the order.
Validation doesn't need to create the order.
The controller doesn't need to implement rate limiting.
This separation makes the application easier to maintain.
16. Middleware Ordering Matters
The order in which middleware runs can affect application behavior.
For example:
app.use(logger);
app.use(authenticate);
app.use(router);
means:
Logger
↓
Authentication
↓
Router
But:
app.use(router);
app.use(logger);
means the logger may not run for requests that are already handled by the router.
Similarly, body-parsing middleware generally needs to run before code that expects the parsed request body.
So middleware isn't just a collection of independent functions.
It's an ordered pipeline.
17. Middleware Can Stop a Request
Middleware doesn't always call next().
For example:
function authenticate(req, res, next) {
if (!req.user) {
return res.status(401).json({
error: "Unauthorized"
});
}
next();
}
If authentication fails:
Request
↓
Authentication
↓
FAILED
↓
401 Response
The controller never runs.
The same pattern works for validation:
Invalid Input
↓
400 Response
and rate limiting:
Too Many Requests
↓
429 Response
This ability to stop a request is one of the most important characteristics of middleware.
18. Middleware and the Controller Have Different Jobs
A useful distinction is:
Middleware
↓
Prepare, inspect, protect, or filter the request
Controller
↓
Handle the actual operation
For example:
GET /api/profile
↓
Authentication
↓
Validation
↓
Controller
↓
Get Profile
Middleware handles the common concerns.
The controller handles the specific request.
This keeps responsibilities separated.
19. Middleware and Services
In a larger backend, the flow can become:
Request
↓
Middleware
↓
Router
↓
Controller
↓
Service
↓
Repository
↓
Database
For example:
Authentication
↓
Validation
↓
Order Controller
↓
Order Service
↓
Inventory Service
↓
Database
The middleware layer shouldn't become a place where all business logic is dumped.
Its purpose is generally to handle concerns around request processing, while business logic belongs in appropriate services or domain layers.
20. A Real Request Example
Imagine a user clicks "Place Order".
The frontend sends:
POST /api/orders
Authorization: Bearer <token>
Content-Type: application/json
with:
{
"productId": 42,
"quantity": 2
}
The backend might process it like this:
Client
↓
POST /api/orders
↓
Logging Middleware
↓
Rate Limiting
↓
Authentication
↓
Validation
↓
Router
↓
Order Controller
↓
Order Service
↓
Inventory
↓
Database
↓
Response
If authentication fails:
Authentication
↓
401
If validation fails:
Validation
↓
400
If the rate limit is exceeded:
Rate Limiter
↓
429
If the database fails:
Database
↓
Error Middleware
↓
500
The controller only receives the request when the earlier stages allow it to continue.
21. Middleware Is a Pipeline
The easiest way to remember middleware is to think of it as a pipeline.
Request
↓
┌───────────┐
│ Logger │
└─────┬─────┘
↓
┌───────────┐
│ Auth │
└─────┬─────┘
↓
┌───────────┐
│ Validation│
└─────┬─────┘
↓
┌───────────┐
│Rate Limit │
└─────┬─────┘
↓
┌───────────┐
│ Controller│
└─────┬─────┘
↓
Response
Every stage gets an opportunity to process the request.
A middleware can:
Continue
↓
Modify
↓
Reject
↓
Pass an error
That's why middleware is such a powerful abstraction.
22. The Bigger Backend Picture
When you combine routing and middleware, a backend starts looking like this:
Client
↓
Load Balancer
↓
Backend Server
↓
Logging
↓
Rate Limiting
↓
Authentication
↓
Validation
↓
Router
↓
Controller
↓
Service
↓
Cache / Database / External APIs
↓
Response
Each layer has a different responsibility.
Routing answers:
Where should this request go?
Middleware answers:
Should this request continue, and what should happen before it reaches the handler?
The controller answers:
What operation should be performed?
The service answers:
What business logic should execute?
The database or external services provide the required data or operations.
23. The Real Reason Backend Developers Use Middleware
The biggest benefit of middleware isn't simply that it makes code shorter.
It's that it creates separation of concerns.
Without middleware:
Controller
├── Authentication
├── Logging
├── Validation
├── Rate Limiting
├── Business Logic
├── Database
└── Error Handling
Everything ends up in one place.
With middleware:
Authentication
↓
Logging
↓
Validation
↓
Rate Limiting
↓
Controller
↓
Business Logic
Each part has a clearer responsibility.
That makes the backend easier to read, test, debug, and extend.
A Simple Mental Model
Whenever a request reaches your backend, think:
Request
↓
Can we identify the request?
↓
Should we allow it?
↓
Is the request valid?
↓
Is the client within limits?
↓
Which route should handle it?
↓
Which controller should run?
↓
What business logic is required?
↓
What response should we return?
Middleware sits in the middle of this process.
It acts as a set of checkpoints between the incoming request and the actual application logic.
That's why backend developers use it so heavily.
The next time you see:
app.get(
"/api/profile",
authenticate,
validate,
getProfile
);
don't think of it as just a list of functions.
Think of it as a pipeline:
Request
↓
Authentication
↓
Validation
↓
Controller
↓
Response
Middleware is the layer that keeps common request-processing logic out of your business logic.
And as a backend grows from a few endpoints to hundreds of APIs and multiple services, that separation becomes increasingly valuable.
Read original: https://dev.to/tanu_priya/what-is-middleware-and-why-do-backend-developers-use-it-5hb0
← Previous
AI has made programming so boring
Next →
Does Market Fear Actually Predict Trader Losses? I Tested It With Real Hyperliquid Data
Related
Automating Google Sites: What Worked, What Failed, and What Cost a Rebuild
Frontend
0
Dev.to (EN Zone)
Advanced JSON Path Operations in WebForms Core 2.1
Frontend
2
DEV Community
LibreOffice Base survey results
Frontend
0
LWN.net
Tencent EdgeOne Makers: Deploying a Static Web Project with HTML, CSS, and JavaScript
Frontend
2
DEV Community
Comments0
No comments yet — be the first