Back to course

What Middleware Really Is

Middleware is just a function that runs between request and response.

Think of your backend like an assembly line:

Request → Middleware → Middleware → Route → Response
/\* IMAGE PLACEHOLDER */ /\* ANIMATION PLACEHOLDER */

If routing is where things go, middleware is what happens along the way.

This is the moment Express usually “clicks”.


The Middleware Signature

Every middleware looks like this:

(req, res, next) => {
  next();
};
  • req → incoming request
  • res → outgoing response
  • next() → pass control to the next middleware

If you don’t call next() (or send a response), the request stops.

/\* IMAGE PLACEHOLDER */

Built-In Middleware

Express ships with some common middleware.

JSON body parser

app.use(express.json());

Without this, req.body is undefined.

Serving static files

app.use(express.static("public"));

Now files in /public are accessible from the browser.

/\* EXERCISE \*/ Try removing express.json() and logging req.body to see what happens.


Custom Middleware

Let’s create a simple request logger:

const logger = (req, res, next) => {
  console.log(req.method, req.path);
  next();
};

app.use(logger);

Now every request prints:

GET /users
POST /login

This middleware runs for all routes because it’s registered with app.use.

Middleware order matters: top to bottom.

/\* IMAGE PLACEHOLDER */

Auth Middleware (Protected Routes)

Auth is just middleware.

Example:

const auth = (req, res, next) => {
  const token = req.headers.authorization;

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

  next();
};

Apply it to a route:

app.get("/profile", auth, (req, res) => {
  res.json({ user: "authenticated" });
});

Mental model:

  • middleware decides access
  • routes handle business logic

Clean separation.

/\* EXERCISE \*/ Add auth to multiple routes and experiment with missing headers.


Error-Handling Middleware

Error middleware looks slightly different:

const errorHandler = (err, req, res, next) => {
  res.status(500).json({ error: err.message });
};

app.use(errorHandler);

Notice the four parameters.

To trigger it:

throw new Error("Something broke");

Or:

next(new Error("Invalid request"));

Now all errors flow to one place.

This gives you: - consistent responses - cleaner routes - easier debugging

Centralized errors are a professional backend pattern.

/\* IMAGE PLACEHOLDER */

Validation Middleware

Validation is also middleware:

const validateUser = (req, res, next) => {
  if (!req.body.email) {
    return res.status(400).json({ error: "Email required" });
  }

  next();
};

Attach it:

app.post("/users", validateUser, (req, res) => {
  res.json({ created: true });
});

Each middleware handles one responsibility.


Interactive Exercise: Middleware Flow

Try this: - Add multiple middleware - Change their order - See how requests move through the pipeline


Mini Project

Build an API with:

1. Request Logger

Logs method + path for every request.

2. Auth Middleware

Protect at least one route.

3. Validation Middleware

Validate POST request body.

4. Centralized Error Handler

Catch all thrown errors in one place.

Suggested structure:

src/
  middlewares/
    auth.js
    logger.js
    validate.js
    errorHandler.js
  routes/
  index.js

/\* PROJECT EXTENSION \*/ Add rate limiting or request timing as bonus middleware.

Goal: small files, clear responsibilities.


Key Takeaways

  • Middleware is a pipeline, not magic
  • Order matters
  • Auth, logging, validation, errors are all the same pattern
  • Routes handle business logic
  • Middleware handles cross-cutting concerns

You now understand how real backends are structured.