Start.dev
Back to blog

Express Middleware Explained for Beginners

Learn what Express middleware is, how next() moves a request through the pipeline, and how to write a logger, an API-key gate and an error handler.

Ariel Weinberger
Ariel Weinberger ยท 4 min read

Sooner or later every Express tutorial says the word "middleware" as if you already know it. You add app.use(express.json()) because the tutorial says so, and it works, but nobody explains what just happened.

Middleware is simpler than its name. It is a function that runs on a request before your route handler does, and it decides whether the request moves on. Once that clicks, logging, authentication and error handling all stop feeling like magic.

This guide assumes you have built at least one Express route. If you have not, start with Build a REST API with Node.js and Express. It also explains how a Node server starts up and listens for requests, and how start.dev runs it for you: there is nothing to install, you open the editor, write code, and it works. Everything below runs the same way.

What middleware actually is

Picture an airport. You do not walk straight onto the plane. You pass check-in, then security, then the gate, and any of them can stop you. Your plane is the route handler. The checkpoints are middleware.

In Express, a request travels through a chain of functions, in the order you registered them. Each one can:

  • Read or change the request (req) and the response (res).
  • Pass the request along by calling next().
  • End the request early by sending a response itself.

That chain is called the pipeline. The lesson The Pipeline And Next (opens in a new tab) walks through it with live requests.

Your first middleware: a logger

A middleware function takes three arguments: req, res and next. Here is one that logs every request:

import express from "express"
 
const app = express()
 
function logger(req, res, next) {
  console.log(`${req.method} ${req.url}`)
  next()
}
 
app.use(logger)
 
app.get("/api/books", (req, res) => {
  res.json([{ id: 1, title: "Dune" }])
})
 
app.listen(3000)

Send a GET to /api/books and the console prints GET /api/books before the response goes out. app.use(logger) means "run this for every request". The call to next() is the important part: it hands the request to the next function in the chain.

You can see this one built step by step in The Logger (opens in a new tab).

The most common bug: forgetting next()

Remove next() from the logger and send a request. The log line prints, and then the request hangs forever. The client never gets a response, because your middleware neither passed the request on nor answered it.

Every middleware must do exactly one of two things: call next(), or send a response. If you do neither, the request is stuck. If you do both, you get the dreaded "headers already sent" error.

Stopping a request on purpose: an API-key gate

The useful trick is that middleware can say no. This one blocks any request without the right key:

function requireApiKey(req, res, next) {
  if (req.get("x-api-key") !== "let-me-in") {
    return res.status(401).json({ error: "Missing or invalid API key" })
  }
  next()
}
 
app.use("/api", requireApiKey)

If the header is wrong, the middleware responds with a 401 and returns. Your route handlers never run. If the header matches, next() lets the request through. Passing "/api" to app.use limits the gate to paths that start with /api, so a public home page stays open.

The key here is hard-coded to keep the example small. A real app reads it from an environment variable. The API Key Gate (opens in a new tab) covers the full version.

Order matters

Express runs middleware top to bottom, so the order you write things in is the order they run:

app.use(express.json())   // 1. parse the JSON body
app.use(logger)           // 2. log the request
app.use("/api", requireApiKey) // 3. check the key
 
app.post("/api/books", (req, res) => {
  res.status(201).json(req.body) // 4. req.body is ready to use
})

Move express.json() below the route and req.body is undefined, because the body had not been parsed yet when the handler ran. Put the API-key gate after a route and that route is unprotected. When something behaves strangely, check the order first. The same rule applies to routes themselves, as Route Order (opens in a new tab) shows.

In fact, express.json() is just middleware someone else wrote. Now you know what that line has been doing all along.

Error-handling middleware

When something throws, you want one place that turns the failure into a clean response instead of a stack trace. Error handlers are middleware with four arguments, with err first:

app.get("/api/crash", (req, res) => {
  throw new Error("Something broke")
})
 
app.use((err, req, res, next) => {
  console.error(err.message)
  res.status(500).json({ error: "Internal server error" })
})

Express recognizes an error handler by that four-argument signature, so you must list all four even if you never use next. It must also be registered last, after all your routes, so errors from anywhere above reach it. You can also pass an error on yourself with next(err). See The Error Handler (opens in a new tab) for the details.

Common middleware mistakes

  • Forgetting next(). The request hangs with no error. Check this first.
  • Calling next() after sending a response. Use return res.status(...) so the function stops there.
  • Wrong order. Body parsing and logging go before routes. The error handler goes after them.
  • Three arguments in an error handler. Express will treat it as normal middleware and never call it with an error.

Practice it

Reading about the pipeline is one thing. Wiring a logger, a gate and an error handler into a working API is what makes it stick. Project Arcade Scoreboard (opens in a new tab) has you do exactly that, and Middleware Knowledge Check (opens in a new tab) lets you test yourself afterwards.

If you are still weighing up which language to build your backend in, Python vs JavaScript: which should you learn first? lays out the trade-offs. And if the JavaScript in this post, especially functions passed as arguments, felt shaky, shore that up first. Middleware is just functions handing work to other functions.

Keep going

Middleware is the idea that turns a handful of routes into a real backend: small functions, each doing one job, chained in order. The full course, Building APIs with Node.js & Express, takes you from here to a structured, persistent API through hands-on projects, all in your browser.

Level up your coding skills

Jump into interactive courses and start building real-world projects.

Start learning

Related posts

Build your first REST API with Node.js and Express in about 50 lines: routes, JSON, POST, PUT and DELETE, tested with curl, plus where to go next.
Ariel Weinberger
Ariel Weinberger ยท 5 min read
Python or JavaScript as your first language? See the same program written both ways, where each trips beginners up, and why JavaScript takes you furthest.
Ariel Weinberger
Ariel Weinberger ยท 9 min read
Learn how REST APIs work by following one request from client to server and back: URLs, methods, headers, status codes, JSON, and what REST actually means.
Ariel Weinberger
Ariel Weinberger ยท 7 min read