What is Middleware in Express and How It Works

I like making things with code. This is where I share my projects and the bugs I ran into.
My JEE Advanced exam, basically India's toughest engineering entrance, was the next day, and since the center was in another city, we were staying at a relative's house. Pretty relatable, na, half of us have done this exam-in-another-city thing. I was worried, obviously, but the night before I just put on my IEMs, my in-ear earphones, and got lost in some music, that is honestly my favourite way to kill stress, and it did the job. So that day passed okay.
Then came the exam morning. By 6 we were getting ready to leave for the center, a private college a bit outside the city. And thoda struggle hua, the cab guy wanted 2,500 rupees when Uber was clearly showing 1,300. I asked him kyun bhai, and he said he would not get any passengers on the way back from out of city, so. I was like why is that my problem, I will pay what Uber officially shows. After a solid ten minutes of arguing we settled around 2,000, and I just let it go, exam ke liye der ho rahi thi, ye itna important nahi hai.
Reached the center, handed my bag to my brother who had come along, and kept only what was allowed, admit card, Aadhaar card, a pen. I walked in, found my room from the seating chart using my roll number, and then came the part everyone who has given a big exam knows too well. The checkpoints. First they checked my ID. Then facial recognition, passed. Then the biometric fingerprint, and yaar, it just would not scan. My finger, the machine, kuch to problem thi. I panicked a little, only a few minutes left, but the invigilator calmly helped, told me to rub my finger, wash it with a little water, try again. I did, and finally it clicked. Metal detector and frisking were already done. I got to my seat with barely two minutes to spare, heart still racing.
Now stop and look at what actually happened between me walking in and me sitting at my seat. I did not go straight to the seat. I passed through a line of checkpoints, ID, then face, then fingerprint, then frisking, each doing one job and passing me to the next, and any single one of them could have stopped me cold, exactly like that fingerprint almost did. That whole line of checkpoints sitting between the entrance and your seat, that is middleware in Express. Chalo, let us understand it properly, and finally solve that app.use(express.json()) mystery I have been dodging for three posts.
First, a quick recall of the request lifecycle
From the earlier posts you already know the basic flow. A request comes in, Express routes it to the right handler, and the handler sends a response. Request in, response out.
But here is the thing I quietly skipped. Between the request arriving and your handler running, there is usually a whole line of functions that run first. Those in-between functions are middleware. Your handler is the seat. Middleware is every checkpoint you clear before you reach it.
What middleware actually is
A middleware is just a function that runs in the middle, between the incoming request and the final route handler. That is literally where the name comes from, it sits in the middle.
Every middleware function gets three things: req, res, and a new one, next.
function myMiddleware(req, res, next) {
// do something with req or res
next(); // then pass control to the next checkpoint
}
req and res you already know from before, the request and the response. The new one, next, is a function you call to say "I am done, send this request on to the next checkpoint." That is the ID guard waving you toward the facial recognition desk. Hold on to next, because it is the whole heart of how middleware works, and we will come back to it.
The key mental picture: a middleware can look at the request, change it, log it, check it, block it, even attach new info onto req for the next checkpoints and the handler to use, and then either pass it along with next(), or stop it right there. Just like a checkpoint.
Where middleware sits in the request lifecycle
So the real flow of a request is not just "request goes to handler." It is "request passes through a pipeline of middleware, in order, and only then reaches the handler."
Request comes in, it hits the first middleware, which does its job and calls next(). Then the second middleware runs, does its job, calls next(). And so on, down the line, until finally the request reaches your route handler, which sends the response. My exam entry, in code form.
The role of next(), the heart of it all
This is the part that makes everything click, so slow down here.
Each middleware has a choice. It can call next() and pass the request onward. Or it can NOT call next(), and stop the request right there.
Remember my fingerprint checkpoint? It refused to clear me. Until that scan passed, I could not move forward, my seat was right there but completely out of reach, I was just stuck at that one gate. That is exactly a middleware that does not call next(). The request halts at that checkpoint and never reaches your handler. Only once the fingerprint finally worked, once next() got called, did I move on and reach my seat.
So a middleware stops the request in one of two ways. Either it sends a response itself and ends things right there, like a guard saying "wrong ID, you cannot enter, go back." Or, if it just forgets to call next() and does not respond either, the request hangs forever, stuck at that gate with nobody moving it forward, which is a bug, and a very common one.
function checkPass(req, res, next) {
if (allGood) {
next(); // cleared, move to the next checkpoint
} else {
res.status(401).send("Not allowed"); // stop right here, send them back
}
}
Ek line mein: call next() to pass the request on, or respond to stop it. That single decision is what every middleware is really doing.
Writing your very first middleware
Enough theory, let us write one. The simplest, most common middleware, a logger that prints every request that comes in. Think of it as the invigilator noting your name and time as you walk in, then waving you on.
const express = require("express");
const app = express();
// our first middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next(); // note it, then wave the request on
});
app.get("/", (req, res) => {
res.send("Home page");
});
app.listen(3000, () => console.log("Server on http://localhost:3000"));
That app.use(...) is how you register a middleware that runs on every request. Hit any route now, and before the handler runs, your logger prints the method and URL, then next() passes control to the handler. It observes and waves you on, never blocks. Simple, and you will use this exact pattern constantly.
Execution order, and why it matters a lot
Here is a rule you must feel in your bones: middleware runs in the exact order you register it, top to bottom.
Just like my exam, the sequence was fixed. ID first, then face, then fingerprint. You could not do fingerprint before ID. Same in Express, the middleware you write first runs first.
app.use((req, res, next) => {
console.log("1. I run first");
next();
});
app.use((req, res, next) => {
console.log("2. I run second");
next();
});
app.get("/", (req, res) => {
console.log("3. Finally the handler");
res.send("Done");
});
Hit / and the console prints 1, then 2, then 3, always in that order. This ordering is not a small detail, it is everything. Put your logger before your auth check and it logs everyone. Put a body-parser after the handler that needs the body, and the body will not be ready in time. Order galat, sab galat.
The types of middleware
Middleware comes in a few flavours. You do not need to overthink these, but knowing the names helps.
Application-level middleware is what you attach to your whole app with app.use(...), so it runs on every request. Our logger above is exactly this. The invigilator at the main entrance that everyone passes.
Router-level middleware is the same idea, but attached to a specific router instead of the whole app, so it only runs for the routes on that router. Think of a checkpoint that only guards one particular block of rooms, not the whole college. You will meet routers properly when your app grows into many files.
Built-in middleware is the ready-made kind Express ships with. And yaar, this is the one you have been waiting for. express.json() is built-in middleware. That mysterious line from every past post,
app.use(express.json());
is just a middleware that runs on every request, reads the JSON body the client sent, and neatly places it on req.body for your handler to use. It is the checkpoint that opens your sealed bag and lays the contents out on the table, so that by the time you reach your handler, req.body is ready. Without it, that bag stays sealed and req.body is undefined, which is exactly why your POST data kept vanishing in the earlier posts. Mystery solved.
There is also express.static(), another built-in, which serves image and CSS files straight from a folder. Same idea, ready-made middleware you just plug in.
And ek aur cheez you will run into fast, third-party middleware. These are ready-made checkpoints written by other people and installed from npm, like cors or morgan. You app.use them exactly the same way as everything else. Someone else built the checkpoint, tumne bas apni line mein laga diya. So when you see app.use(cors()) in real code, ab pata hai, it is just another middleware in the pipeline.
The real-world jobs middleware does
Now the fun part, what people actually use middleware for. Look back at my exam and you have basically already seen them all.
Logging. Recording every request that hits your server, method, URL, time. Great for debugging and for seeing what your app is doing. This is the invigilator noting each entry. It never blocks, it just observes and calls next().
Authentication. This is the ID, face, and fingerprint gauntlet. Their one job was to confirm I am really the person allowed to sit here. An auth middleware does the same, it checks something like a token or a key, and if it is missing or wrong, it stops the request with a 401, sending you back like a wrong ID would. If it is valid, it calls next() and you proceed. And here is a bonus you will use constantly, once it verifies you, an auth middleware usually attaches the verified user right onto the request, like req.user, so every handler after it instantly knows who is asking, without checking again. Socho, once my fingerprint cleared, the system knew exactly who had walked in, and that identity stayed with me all the way to my seat. Same thing, req.user carries who you are down the pipeline. This is how you protect private routes and know who is behind each one.
Request validation. The metal detector and frisking, making sure you are not carrying anything you should not. A validation middleware checks that the incoming request has the right shape and data, for example that a signup request actually includes an email and a password, and rejects it early with a clear error if not, before your handler ever runs.
See the pattern? Middleware is where you put all the "check this on the way in" logic, so your actual route handlers stay clean and only deal with the real work.
Building it all in Express
Let us put logging, auth, and a protected route together in one small server, so you see the whole pipeline working as one.
const express = require("express");
const app = express();
// built-in middleware: parse JSON bodies (the mystery, finally solved)
app.use(express.json());
// application-level middleware: log every request
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next(); // note it, wave it on
});
// our own auth-check middleware (the ID / fingerprint checkpoint)
function checkAuth(req, res, next) {
const key = req.headers["x-api-key"];
if (key !== "secret123") {
return res.status(401).json({ error: "Checkpoint failed, not allowed" });
}
next(); // verified, proceed to your seat
}
// public route, no checkpoint needed
app.get("/", (req, res) => {
res.send("Anyone can see this");
});
// protected route, must clear checkAuth first
app.get("/dashboard", checkAuth, (req, res) => {
res.send("Welcome to your dashboard");
});
app.listen(3000, () => console.log("Server on http://localhost:3000"));
Read the flow. Every request first gets its body parsed by express.json(), then logged by our logger, both call next() and pass it on. A plain / request sails straight to its handler. But a /dashboard request has one extra checkpoint, checkAuth, sitting right before its handler. Notice how it is placed, app.get("/dashboard", checkAuth, handler), the middleware goes in between the path and the handler, so it runs only for this route. If the key is wrong, checkAuth stops the request with a 401 and the handler never runs, my failed fingerprint. If the key is right, next() runs and you reach the dashboard, my seat.
Try it yourself
Do not just read it, khud chala ke dekho. Make a folder, run npm init -y and npm install express, drop the code above into index.js, and run node index.js. Keep an eye on your terminal for the logger.
# public route, works for anyone
curl http://localhost:3000/
# protected route with no key, checkpoint stops you (401)
curl http://localhost:3000/dashboard
# protected route with the right key, you get in
curl -H "x-api-key: secret123" http://localhost:3000/dashboard
Watch it. The first works, the second gets blocked at the checkpoint just like my fingerprint, and the third clears it and reaches the handler. Then try the classic bug on purpose, remove the next() from the logger and hit any route, and watch your request hang forever with no response, stuck at a checkpoint nobody waved it past. That hang teaches the lesson better than any paragraph.
The mistakes that will trip you up
The usual middleware traps, so you know them before they bite.
Forgetting next(). The most common one by far. Your middleware does its job but never calls next() and never sends a response, so the request just hangs, loading forever. If a route mysteriously never responds, a missing next() is the first thing to check.
Wrong order. Registering middleware in the wrong sequence. An auth check placed after the route it was meant to protect will not protect it. A body parser placed after a handler that needs the body leaves req.body empty. Order is everything, top to bottom.
Calling next() after sending a response. Doing res.send(...) and then also calling next(), or sending two responses. You get the "Cannot set headers after they are sent" error from the earlier posts. One checkpoint, one decision, pass or stop, not both.
Forgetting express.json(). Yes, still. If req.body is undefined on a POST, this built-in middleware is missing. Now you know it is a checkpoint you simply did not add.
Quick reference, bookmark this bit
The whole post in a thirty-second scan.
A middleware is a function (req, res, next) that runs between the request and the route handler. It can inspect or change the request, then either next() to continue or send a response to stop.
// the shape of every middleware
function middleware(req, res, next) {
// do something
next(); // pass on, OR res.send(...) to stop
}
app.use(middleware); // runs on every request
app.get("/x", middleware, handler); // runs only for this route
| Type | How | Example |
|---|---|---|
| Application-level | app.use(fn) |
logging on every request |
| Router-level | router.use(fn) |
guards one router's routes |
| Built-in | ships with Express | express.json(), express.static() |
Common jobs: logging, authentication (block with 401 if invalid), request validation.
The rules worth memorizing. Middleware sits between request and handler. It gets req, res, next. Call next() to pass the request on, or send a response to stop it. Middleware runs top to bottom, in the exact order you register it. And express.json() is just built-in middleware that fills req.body. Forget next() and the request hangs forever.
Wrapping up
So that is middleware. It is the line of checkpoints every request passes through before it reaches your route handler. Each middleware gets req, res, and next, does one small job, logging, authenticating, validating, parsing the body, and then either waves the request on with next() or stops it by sending a response. They run in the order you register them, and they are where all your "check this before the real work" logic belongs, keeping your handlers clean.
That is exactly my exam entry. I did not teleport to my seat, I cleared ID, face, fingerprint, and frisking, one after another, each a small check, and my seat, the route handler, only came after all of them passed. And that one failed fingerprint that froze me in place? That was a middleware refusing to call next(), the whole request stuck at a single checkpoint until it cleared. Two minutes to spare, but I made it, and now you finally know why app.use(express.json()) was there all along.
Next up, now that you understand the middleware pipeline, we use it for one of the most real-world jobs there is, handling file uploads with a middleware called Multer. That is my next post, Handling File Uploads in Express with Multer.
I hope you enjoyed reading this.



