DEV Community

Cover image for What Is Middleware and Why Do Backend Developers Use It?
Tanu Priya
Tanu Priya

Posted on

What Is Middleware and Why Do Backend Developers Use It?

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

The important part is:

next();
Enter fullscreen mode Exit fullscreen mode

Calling next() tells Express:

"I'm done with my work. Continue processing this request."

So the flow becomes:

Request
   ↓
logger()
   ↓
next()
   ↓
Controller
   ↓
Response
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For example:

function authenticate(req, res, next) {

    if (!req.user) {
        return res.status(401).json({
            error: "Unauthorized"
        });
    }

    next();
}
Enter fullscreen mode Exit fullscreen mode

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

});
Enter fullscreen mode Exit fullscreen mode

Now imagine 30 different protected endpoints.

You might end up repeating:

Authentication
Authorization
Logging
Validation
Rate limiting
Enter fullscreen mode Exit fullscreen mode

inside many controllers.

That creates duplicated code.

Middleware lets you move reusable responsibilities into separate functions:

Request
   ↓
Authentication
   ↓
Rate Limiting
   ↓
Validation
   ↓
Controller
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Then:

app.get(
    "/api/profile",
    authenticate,
    getProfile
);
Enter fullscreen mode Exit fullscreen mode

The flow becomes:

Request
   ↓
Authentication Middleware
   ↓
Token Valid?
   |
   ├── No → 401
   |
   └── Yes
        ↓
     Controller
        ↓
     Response
Enter fullscreen mode Exit fullscreen mode

The controller doesn't need to worry about how authentication works.

It can simply use:

req.user
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You might have:

app.delete(
    "/api/users/:id",
    authenticate,
    requireAdmin,
    deleteUser
);
Enter fullscreen mode Exit fullscreen mode

The middleware chain becomes:

Request
   ↓
authenticate
   ↓
requireAdmin
   ↓
deleteUser
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

A simple middleware could be:

function logger(req, res, next) {

    console.log(
        req.method,
        req.originalUrl
    );

    next();
}
Enter fullscreen mode Exit fullscreen mode

Then:

app.use(logger);
Enter fullscreen mode Exit fullscreen mode

Now every request passes through it.

The flow becomes:

GET /api/products
       ↓
Logger
       ↓
Router
       ↓
Controller
       ↓
Response
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then different parts of the system can log:

request_id = abc123
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

But the client sends:

{
  "name": "",
  "age": "hello"
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Then:

app.post(
    "/api/users",
    validateUser,
    createUser
);
Enter fullscreen mode Exit fullscreen mode

The flow becomes:

Request
   ↓
Validation
   ↓
Valid?
   |
   ├── No → 400
   |
   └── Yes
        ↓
   Controller
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For example:

POST /api/products
Enter fullscreen mode Exit fullscreen mode

might require:

name → required
price → positive number
category → allowed value
description → maximum length
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Conceptually:

Request
   ↓
Rate Limiter
   ↓
Limit exceeded?
   |
   ├── Yes → 429 Too Many Requests
   |
   └── No
        ↓
      Router
Enter fullscreen mode Exit fullscreen mode

The standard HTTP response for rate limiting is:

429 Too Many Requests
Enter fullscreen mode Exit fullscreen mode

Rate limiting is particularly useful for endpoints such as:

/login
/signup
/password-reset
/search
/public APIs
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    });
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Another endpoint might return:

{
  "error": "Database error"
}
Enter fullscreen mode Exit fullscreen mode

Another might return:

{
  "success": false
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Later:

function getProfile(req, res) {

    const userId = req.user.id;

    // Fetch profile
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Or only to specific routes:

app.get(
    "/api/profile",
    authenticate,
    getProfile
);
Enter fullscreen mode Exit fullscreen mode

Or to an entire group of routes:

app.use(
    "/api/admin",
    authenticate,
    requireAdmin,
    adminRoutes
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

if that endpoint is intentionally public.

But you probably do want it on:

GET /api/profile
Enter fullscreen mode Exit fullscreen mode

15. Middleware Chains

The real power comes from combining middleware.

Consider:

app.post(
    "/api/orders",
    authenticate,
    rateLimit,
    validateOrder,
    createOrder
);
Enter fullscreen mode Exit fullscreen mode

The request travels through:

POST /api/orders
       ↓
Authentication
       ↓
Rate Limiting
       ↓
Validation
       ↓
Controller
       ↓
Order Service
       ↓
Database
       ↓
Response
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

means:

Logger
  ↓
Authentication
  ↓
Router
Enter fullscreen mode Exit fullscreen mode

But:

app.use(router);
app.use(logger);
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

If authentication fails:

Request
   ↓
Authentication
   ↓
FAILED
   ↓
401 Response
Enter fullscreen mode Exit fullscreen mode

The controller never runs.

The same pattern works for validation:

Invalid Input
     ↓
400 Response
Enter fullscreen mode Exit fullscreen mode

and rate limiting:

Too Many Requests
     ↓
429 Response
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For example:

GET /api/profile
       ↓
Authentication
       ↓
Validation
       ↓
Controller
       ↓
Get Profile
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For example:

Authentication
      ↓
Validation
      ↓
Order Controller
      ↓
Order Service
      ↓
Inventory Service
      ↓
Database
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

with:

{
  "productId": 42,
  "quantity": 2
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If authentication fails:

Authentication
      ↓
401
Enter fullscreen mode Exit fullscreen mode

If validation fails:

Validation
      ↓
400
Enter fullscreen mode Exit fullscreen mode

If the rate limit is exceeded:

Rate Limiter
      ↓
429
Enter fullscreen mode Exit fullscreen mode

If the database fails:

Database
   ↓
Error Middleware
   ↓
500
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Every stage gets an opportunity to process the request.

A middleware can:

Continue
   ↓
Modify
   ↓
Reject
   ↓
Pass an error
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Everything ends up in one place.

With middleware:

Authentication
      ↓
Logging
      ↓
Validation
      ↓
Rate Limiting
      ↓
Controller
      ↓
Business Logic
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

don't think of it as just a list of functions.

Think of it as a pipeline:

Request
   ↓
Authentication
   ↓
Validation
   ↓
Controller
   ↓
Response
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)