# The Guard at Your API’s Gate : Middleware in Express, explained simply

## Introduction

In a real application, a lot of work happens before the route runs like logging the request, reading the JSON body, checking if the user is logged in and validating the input.

If we write all of this inside every route, the code becomes repetitive very fast. Express solves this with middleware.

In this blog, we will understand what middleware is, how `next()` works, why order matters, and how to use the different types of middleware in real projects.

* * *

## What is Middleware?

A middleware is a function that runs between the request and the response. It receives three things:

*   `req`: the request
    
*   `res`: the response
    
*   `next`: a function that passes control to the next step
    

```javascript
function myMiddleware(req, res, next) {
  // do something with req or res
  next(); // move to the next step
}
```

A middleware can do four things:

1.  Run any code.
    
2.  Change `req` and `res` (for example, add `req.user`).
    
3.  End the request by sending a response.
    
4.  Pass the request forward by calling `next()`.
    

Think of it as a security check at an airport. Before you reach the gate (the route handler), you pass through a few checks: ticket, ID, baggage. If any check fails, you are stopped there. If all pass, you reach the gate.

### Two Forms of Middleware

Express has two forms of middleware. Express tells them apart by the **number of parameters**.

```javascript
// Normal middleware: 3 parameters
function myMiddleware(req, res, next) {
  next();
}

// Error-handling middleware: 4 parameters
function myErrorHandler(err, req, res, next) {
  res.status(500).json({ error: 'Something went wrong' });
}
```

*   **Normal middleware** has 3 parameters: `req`, `res`, `next`.
    
*   **Error-handling middleware** has 4 parameters: `err`, `req`, `res`, `next`. The first one is always `err`.
    

All four parameters must be written, even if you do not use `err` inside the function. If you remove `err`, only three parameters are left, and Express treats it as a normal middleware.

Many beginners never write an error handler, because Express has a default one built in. But writing your own gives you control: you decide the response, the status code and what gets logged. We will see how in the Error-handling section.

![](https://cdn.hashnode.com/uploads/covers/69513d1ce0cbcddf469383e9/ab528503-14b4-437c-a493-c39951eed6a0.png align="center")

* * *

## Your First Middleware

> **Let\`s say we want to log every request: its method and URL. Without middleware, we would write the log in every route:**

```javascript
app.get('/', (req, res) => {
  console.log(`${req.method} ${req.url}`); // repeated
  res.send('Home page');
});

app.get('/about', (req, res) => {
  console.log(`${req.method} ${req.url}`); // repeated
  res.send('About page');
});

app.get('/users', (req, res) => {
  console.log(`${req.method} ${req.url}`); // repeated
  res.json('Users page');
});
```

The same line is copied three times. With 50 routes, it will be copied 50 times, and if we want to change the log, we must change it in 50 places. Even worse, if we forget it in one route, that request is never logged.

> **Middleware solves this. We write the logic once, and it runs for every request:**

```javascript
import express from 'express';

const app = express();

// Middleware: runs for every request
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();
});

app.get('/', (req, res) => res.send('Home page'));
app.get('/about', (req, res) => res.send('About page'));
app.get('/users', (req, res) => res.json('Users page'));

app.listen(3000, () => console.log('Server running on port 3000'));
```

Now open `/`, `/about` and `/users` in the browser and look at your terminal:

```plaintext
GET /
GET /about
GET /users
```

Notice that the middleware is written before the routes. Express reads from top to bottom, so it must come first.

All three routes are logged, and our routes did not change at all. They stay clean and only do their own job.

`app.use()` registers the middleware for every request. After it prints the log, `next()` passes the request to the next step, which is the route. Any new route we add later is logged automatically.

![](https://cdn.hashnode.com/uploads/covers/69513d1ce0cbcddf469383e9/2558c59c-17a8-4d85-8e2e-f35848f6d223.png align="center")

* * *

## `next()` Explained

`next()` is what keeps the request moving. There are only two possible endings for a middleware:

*   **Call** `next()`: the request continues to the next middleware or route.
    
*   **Send a response**: the request ends here, and nothing after it runs.
    

If you do neither, the request is stuck, and the client waits until it times out.

```javascript
// Wrong: no next() and no response, the request hangs
app.use((req, res, next) => {
  console.log('Request came');
});

// Correct
app.use((req, res, next) => {
  console.log('Request came');
  next();
});
```

Always call `next()` if you want the request to continue. It passes control to the next middleware or route handler. Without `next()` or a response, the request gets stuck, and the client keeps waiting.

```javascript
if (!token) {
  return res.status(401).json({ error: 'No token provided' });
}
next();
```

* * *

## Middleware Execution Order

Middleware runs **in the order you write it**, from top to bottom. This is the same rule as route order from part 1.

```javascript
app.use((req, res, next) => {
  console.log('First');
  next();
});

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

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

A request to `/` prints:

```plaintext
First
Second
Route handler
```

Because of this, **placement matters**. Always put middleware before the routes that need it. Otherwise, it may not run for those requests, and things like `express.json()` won't be able to parse the request body before the route handler uses it.

* * *

## Types of Middleware

### 1\. Application-level Middleware

This is what we have used so far. It is attached to the whole app with `app.use()`.

```javascript
app.use(logger);              // runs for every request
app.use('/api', apiLogger);   // runs only for URLs starting with /api
```

We can also attach a middleware to **one route only**. Just place it before the handler:

```javascript
app.get('/profile', auth, (req, res) => {
  res.json({ message: 'Your profile' });
});
```

You can pass many middlewares in a row. They run left to right:

```javascript
app.post('/users', auth, validateUser, (req, res) => {
  // runs only if auth and validateUser both called next()
});
```

* * *

### 2\. Router-level Middleware

Imagine your app has 10 admin routes. Instead of adding the same middleware to all 10 routes, we can group them together using `express.Router()`.

```javascript
const express = require('express');
const app = express();

const admin = express.Router();

// Runs before every admin route
admin.use((req, res, next) => {
  console.log('Checking admin access...');
  next();
});

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

admin.get('/settings', (req, res) => {
  res.send('Admin Settings');
});

// All admin routes start with /admin
app.use('/admin', admin);
```

### Working:

*   `express.Router()` creates a separate router to keep admin routes together.
    
*   `admin.use()` adds middleware that runs before the admin routes.
    
*   `admin.get('/')` handles the admin dashboard.
    
*   `admin.get('/settings')` handles the settings page.
    
*   `app.use('/admin', admin)` connects this router to the main app. Every route inside it starts with `/admin`.
    
*   So `/` becomes `/admin/`, and `/settings` becomes `/admin/settings`.
    

That\`s how you can also keep group related routes together, add middleware once, and connect the group to your main app.

![](https://cdn.hashnode.com/uploads/covers/69513d1ce0cbcddf469383e9/44351c11-2dc0-47eb-962a-0940eb321fac.png align="center")

* * *

### 3\. Built-in Middleware

Express comes with a few ready-made middlewares.

| Middleware | What it does |
| --- | --- |
| `express.json()` | Reads a JSON body and puts it in `req.body` |
| `express.urlencoded({ extended: true })` | Reads HTML form data and puts it in `req.body` |
| `express.static('public')` | Serves files from a folder (images, CSS, JS) |

```javascript
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
```

**Serving static files:** with `express.static('public')`, a file at `public/style.css` is available at [`http://localhost:3000/style.css`](http://localhost:3000/style.css). We do not write any route for it. The middleware checks if the file exists, sends it, and ends the request. If the file does not exist, it calls `next()` and the request continues to your routes.

We can also add a prefix:

```javascript
app.use('/assets', express.static('public'));
// public/logo.png is now available at /assets/logo.png
```

* * *

### 4\. Error-handling Middleware

This one is special. It has **four** parameters, and the first one is `err`. We will see it in detail below.

* * *

### Third-party Middleware

Many packages from npm are middleware too. For example, `cors` (cross-origin requests), `helmet` (security headers), and `morgan` (request logging). You install them and use them with `app.use()`.

* * *

## Error-handling Middleware

Errors will happen: a database fails, a value is missing, some code throws. Instead of handling errors in every route, we handle them in **one place**.

An error-handling middleware has **four parameters**. Express knows it is an error handler because of the `err` first parameter, so all four must be written, even if you do not use `next`.

javascript

```javascript
app.use((err, req, res, next) => {
  console.error(err.message);
  res.status(err.status || 500).json({ error: 'Something went wrong' });
});
```

It must be placed **after all routes and other middleware**.

To send an error to it, pass the error to `next()`:

```javascript
app.get('/users/:id', (req, res, next) => {
  const user = null; // imagine the database found nothing

  if (!user) {
    const error = new Error('User not found');
    error.status = 404;
    return next(error); // skips all normal middleware, goes to the error handler
  }

  res.json(user);
});
```

In Express 5, if an `async` route throws an error or a promise is rejected, Express passes it to the error handler automatically. In Express 4 you had to catch it and call `next(err)` yourself.

```javascript
app.use((req, res) => {
  res.status(404).json({ error: `Cannot ${req.method} ${req.originalUrl}` });
});
```

* * *

## The Middleware Chain

Now let's put it together. This is a request to delete a user:

```javascript
app.use(express.json());
app.use(logger);

app.delete('/users/:id', auth, requireRole('admin'), (req, res) => {
  // delete the user from the database
  res.status(204).end();
});

app.use(notFound);
app.use(errorHandler);
```

The request passes through:

1.  `express.json()`: reads the body
    
2.  `logger`: prints the request
    
3.  `auth`: checks the token (stops with `401` if it fails)
    
4.  `requireRole('admin')`: checks the role (stops with `403` if it fails)
    
5.  Route handler: does the real work and sends the response
    

If any step fails, the request ends there, and the later steps never run. Each middleware does **one small job**, and together they form a pipeline.

![](https://cdn.hashnode.com/uploads/covers/69513d1ce0cbcddf469383e9/06175d46-81b8-480a-a67f-8c4d011ed2f3.png align="center")

* * *

You can find more of my work at [abdulrdeveloper.me](http://abdulrdeveloper.me)

Read more posts at [blog.abdulrdeveloper.me](http://blog.abdulrdeveloper.me)
