If you’ve worked with Express long enough, you’ve probably hit bugs that felt… confusing.
A middleware didn’t run. req.body was suddenly undefined. You saw “Cannot set headers after they are sent” and stared at your screen for five minutes wondering what just happened.
Most of these issues come from one thing, not fully understanding how a request actually flows through an Express app.
Let’s break that flow down step by step, without hand-wavy explanations.
🚀 Master the core mechanics of backend engineering—including Express, databases, and system architecture—by joining the Complete Backend Course today.
At its core, Express isn’t magical. It’s a pipeline. An incoming HTTP request enters your app and moves through a stack of functions, one after another. Each function decides whether the request continues forward or stops right there.
High level, the lifecycle looks like this:


Once you understand this mental model, most Express behavior stops feeling random and starts feeling predictable.
Everything starts at Node’s HTTP server. When a request arrives, Express wraps it into two objects:
These objects are created once and then shared across the entire lifecycle of the request. Every middleware and every route handler receives the exact same req and res. Any mutation you make sticks around for the rest of the request, nothing is copied or reset between steps.
At this moment, the request is still very raw. Headers, method, and URL are available, but the body has not been parsed yet, and no authentication or validation has happened. This is exactly why Express relies so heavily on middleware to prepare the request before it reaches your business logic.
Middleware is just a function that receives req, res, and next. The most important rule to understand is simple: Express runs middleware in the exact order you define it.
app.use(logger);app.use(express.json());app.use(auth);
This order is not optional. Express will not reorder things for you. Each middleware has three possible outcomes:
If a middleware never calls next() and never sends a response, the request just hangs. That’s not a bug in Express, it’s the lifecycle stopping mid-way.
Because middleware mutates req, order changes behavior.
For example:
A very common mistake is putting middleware in the wrong place and expecting Express to “figure it out.” It won’t. Express is intentionally simple. It just walks forward, step by step.
This one surprises a lot of people. Express does not parse request bodies by default.
When you write:
app.use(express.json());
You’re not enabling a feature, you’re adding another middleware to the pipeline.
What it does:
If this middleware doesn’t run, req.body stays undefined. Validation fails, handlers break, and things feel confusing. In practice, body-related bugs almost always come down to missing middleware or incorrect middleware order.
Routes in Express are also middleware. The difference is that they only run when the HTTP method and path match.
Routes are checked in the order they’re defined. The first match wins. A route handler can still choose to call next() and let execution continue, but most don’t.
This explains bugs like:
By the time a request reaches a route handler, it has already passed through every middleware defined before it and is finally ready for business logic.
If you remember the rule that middleware outcomes are either ending the response or calling next(), there’s a secret third option: calling next(err).
When you pass an argument into next(), Express stops normal execution. It abandons any remaining standard middleware or routes and jumps straight down the pipeline looking for an error handler.
But Express doesn't use magic to figure out which middleware is the error handler. It looks for one specific thing: a function signature with exactly four arguments.
app.use((err, req, res, next) => {console.error(err.stack);res.status(500).send('Something broke!');});
If you leave out the next parameter, even if you don't plan on using it, Express will look at the (err, req, res) signature, assume it's just a normal middleware, and skip right over it.
And because order is everything in Express, your error handling middleware must be the absolute last thing you define in your file. It needs to sit at the very bottom of the pipeline, ready to catch any errors that tumble down from the routes above.
Once you stop looking at Express as a black box and start seeing it as a predictable, top-to-bottom pipeline, debugging becomes a whole lot easier.
Missing a body? Check the parser. Route not firing? See what intercepted it above. App hanging? Find the middleware that forgot to call next(). Mastering the request lifecycle doesn't just fix bugs it makes you a fundamentally better backend developer. Next time you see "Cannot set headers after they are sent," you'll know exactly where to look.
🚀 Master the core mechanics of backend engineering—including Express, databases, and system architecture—by joining the Complete Backend Course today.
Join JS Mastery Pro to apply what you learned today through real-world builds, weekly challenges, and a community of developers working toward the same goal.