When you book a train ticket on IRCTC, your request does not travel straight from your phone to "seat confirmed." It passes through checkpoints: first the system checks whether you are logged in, then it checks whether your payment details are valid, then it checks whether a seat actually exists in that quota, and only after all three checks pass does the booking engine touch the database and reserve your berth. If any checkpoint fails — say your session has expired — the request never reaches the booking engine at all; it gets stopped right there and sent back to you with "please log in again."
An Express.js server is built out of exactly this idea, and the checkpoints have a name: middleware. This chapter is about what middleware functions actually are, how a request moves through a chain of them, and the specific, easy-to-make mistakes that trip up almost every beginner the first time they write one.
Quick Recap: What a Route Handler Does
In earlier work with Express you have written things like this:
app.get('/students', (req, res) => {
res.send('List of students');
});
Here, (req, res) => { res.send('List of students'); } is a plain JavaScript function. Express calls it automatically whenever a GET request arrives at /students, handing it two objects: req (information about the incoming request — URL, headers, body) and res (a toolkit for sending a reply back). This function is called a route handler, and it is the last stop for a request — the place where the actual work happens and a response finally goes out.
Middleware functions look almost identical, but they sit before the route handler, not at the end of the line.
The Missing Piece: A Third Parameter Called next
Here is the smallest possible middleware function:
app.use((req, res, next) => {
console.log(`${req.method} ${req.url} received at ${new Date().toISOString()}`);
next();
});
Notice the signature: (req, res, next) — three parameters instead of two. That third parameter, next, is itself a function that Express hands you. Calling next() tells Express: "I am done with my checkpoint duties — send this request on to whatever comes after me." This one detail is the entire mechanism that makes middleware chains work, and we will trace it carefully.
Let's put this logging middleware in front of a real route and follow one request through the whole pipeline, step by step:
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log(`${req.method} ${req.url} received at ${new Date().toISOString()}`);
next();
});
app.get('/students', (req, res) => {
res.send('List of students');
});
app.listen(3000, () => console.log('Server running on port 3000'));
Suppose a browser sends GET /students. Here is exactly what happens, in order:
- Express receives the request and starts walking through the functions you registered, from top to bottom in the order they appear in the file.
- It reaches the
app.use(...)line first — this is registered with no specific path, so it runs for every request, regardless of URL. It executes the function: theconsole.logline prints something likeGET /students received at 2026-08-13T09:12:44.101Zto the server's terminal (the client sees nothing yet — this is server-side logging). - Inside that function,
next()is called. This hands control forward to the next matching layer. - Express now reaches
app.get('/students', ...). The path matches, so this route handler runs: it callsres.send('List of students'), which finally writes the HTTP response and sends it back to the browser. - The request-response cycle is complete.
The important realization: the route handler (req, res) => { res.send(...) } only has two parameters because it never needs to pass control further — it ends the journey by sending a response. The middleware above it has three parameters because its job is to do something small (logging, in this case) and then step aside.
What Happens If You Forget next()?
This is the single most common bug beginners write with Express middleware, so let's build it on purpose and watch it fail. Suppose you write:
app.use((req, res, next) => {
console.log('Checking request...');
// next() was forgotten here
});
app.get('/home', (req, res) => {
res.send('Home page');
});
A request for GET /home arrives. Express reaches the app.use(...) middleware, runs it, and "Checking request..." is printed to the terminal. Then the function simply returns — nothing else happens inside it. Because next() was never called, Express has no instruction to move on to app.get('/home', ...), and because res.send() was never called either, no response was written. The browser tab just keeps spinning, waiting for a reply that is never going to come, until the connection eventually times out on its own.
Common misconception: students often assume that if a middleware function doesn't explicitly reject or block a request, Express will "figure out" that it should move on anyway. It will not. Express has no idea what your function intended to do — the only two ways a request can proceed at all are (a) your function calls next(), handing control to whatever is registered after it, or (b) your function calls one of the response methods (res.send(), res.json(), res.end(), etc.), ending the cycle right there. If a middleware function does neither, the request is stuck forever — this is not a crash, which would at least produce an error message; it is a silent hang, which is often harder to debug precisely because nothing looks obviously wrong in your code at first glance.
Order Is Not a Suggestion — It Is the Program
Express does not look through all your middleware and routes to find the "best match." It checks them in the exact order you registered them, top to bottom, and runs each one that matches the incoming request's path and method, stopping only when a response is sent. This means the physical position of a line in your file changes what your server does. Consider three middleware functions plus a route handler:
app.use((req, res, next) => {
console.log('A: Logger');
next();
});
app.use((req, res, next) => {
console.log('B: Auth check');
req.isLoggedIn = true;
next();
});
app.get('/dashboard', (req, res) => {
console.log('C: Route handler');
res.send(req.isLoggedIn ? 'Welcome to dashboard' : 'Please log in');
});
For a request to GET /dashboard, the terminal prints, in this exact order: A: Logger, then B: Auth check, then C: Route handler — because that is the order the three functions sit in the file. Notice also that B attaches a new property, req.isLoggedIn, directly onto the req object. This is a core technique: req and res are the same two objects passed along the entire chain for one request, so any middleware can attach information to req that a later middleware or route handler reads. Route handler C reads req.isLoggedIn, which only exists because B ran before it and set it. If you swapped the order and put the route handler before the auth-check middleware, req.isLoggedIn would still be undefined when C runs, because the code that sets it would not have executed yet.
The Bug That Order Creates: Body-Parsing Registered Too Late
Here is a realistic, exam-relevant version of the same mistake, involving Express's built-in JSON body parser, express.json(). This middleware reads the raw text of an incoming request body and, if its Content-Type header says application/json, parses it into a JavaScript object available at req.body. Without it, req.body is simply undefined, no matter what the client sent.
const express = require('express');
const app = express();
// Route registered FIRST
app.post('/register', (req, res) => {
console.log(req.body);
res.send('Registered ' + req.body.name);
});
// Body-parsing middleware registered AFTER the route
app.use(express.json());
app.listen(3000);
Suppose a client sends POST /register with a JSON body {"name": "Aisha"} and the header Content-Type: application/json. Trace it: Express walks top to bottom, and the very first matching layer it finds is the /register route handler — because it was registered before app.use(express.json()). That handler runs immediately. Inside it, req.body is undefined, because the middleware that would have parsed the JSON text into an object has not run yet — it appears later in the file, so Express has not reached it. The line console.log(req.body) prints undefined, and the next line, req.body.name, throws a TypeError: Cannot read properties of undefined (reading 'name'), crashing the request with a server error.
The fix is purely a matter of ordering — move the body parser above the routes that depend on it:
app.use(express.json()); // now runs first, for every request
app.post('/register', (req, res) => {
res.send('Registered ' + req.body.name); // req.body.name is now 'Aisha'
});
This single reordering is one of the most frequent real bugs in Express projects, and it exists precisely because middleware order determines what information is available by the time a later handler runs. There is nothing magical protecting you from this — Express simply executes what you wrote, in the sequence you wrote it.
Middleware Scoped to One Route
Not every checkpoint needs to apply to the whole application. You can also pass a middleware function as an extra argument directly into a specific route, so it only runs for that one route:
function checkAdmin(req, res, next) {
if (req.query.role === 'admin') {
next();
} else {
res.status(403).send('Access denied: admins only');
}
}
app.get('/admin-panel', checkAdmin, (req, res) => {
res.send('Welcome, admin');
});
For GET /admin-panel?role=admin, checkAdmin runs first, sees req.query.role === 'admin' is true, calls next(), and the route handler sends the welcome message. For GET /admin-panel with no role query parameter, checkAdmin runs, the condition is false, and it calls res.status(403).send(...) directly — ending the cycle right there. The route handler function never runs at all in this case, because nothing ever called next() to reach it. This is the other legitimate way a middleware function can end a request: instead of passing it forward, it can decide the request should stop here and respond immediately, which is exactly how login checks and permission checks are implemented in real Express applications.
Error-Handling Middleware: A Different Signature On Purpose
Express has one more kind of middleware, and you can recognize it immediately because it takes four parameters instead of three: (err, req, res, next). Express specifically looks at how many parameters a function declares to decide whether it is a normal middleware or an error handler — this is not a naming convention you choose, it is how the framework itself tells the two apart.
app.get('/data', (req, res, next) => {
try {
const result = riskyOperation();
res.json(result);
} catch (err) {
next(err);
}
});
app.use((err, req, res, next) => {
console.error(err.message);
res.status(500).send('Something went wrong on our server');
});
Trace what happens if riskyOperation() throws an error. The catch block catches it and calls next(err) — passing an argument to next for the first time. This single detail changes everything downstream: whenever next() is called with an argument, Express treats it as "an error occurred" and immediately skips every remaining normal middleware and route handler, jumping straight to the nearest error-handling middleware (the one with four parameters) that was registered after it. That handler logs err.message to the server terminal and sends the client a generic 500 response, rather than leaking the raw error or crashing the whole server process. Error-handling middleware is always registered last, after every route, precisely because it needs to sit at the very end of the chain to catch whatever gets passed to it from anywhere earlier.
Seeing the Whole Pipeline
The blue path along the top is the normal, successful journey: each middleware calls next() and hands the request one step to the right, until the route handler finally calls a response method. The red dashed path shows what happens the moment any middleware calls next(err) instead of a plain next() — Express abandons the normal left-to-right chain entirely and jumps straight down to the four-parameter error handler, wherever it happens to be registered, as long as it comes after the point where the error occurred.
Why This Matters for CBSE and Beyond
You have already met the underlying idea in your Class 9 study of functions: a function can be passed as a value into another function and called later — this is exactly what app.use(middlewareFunction) does, and it is why JavaScript functions like next can themselves be passed around and invoked. The Express middleware chain is a direct, practical application of "functions as values," just applied to something concrete: processing one HTTP request through a sequence of independent, reusable checks — logging, parsing, authentication, and error handling — each written once and reused across every route in an application, rather than copy-pasted into every single route handler by hand.
Check Your Understanding
- A middleware function is registered with
app.use((req, res, next) => { console.log('hit'); })— no call tonext()or any response method anywhere inside it. What happens when a request arrives, and why does it not produce an error message? - Given
app.post('/order', handler)registered on line 3 andapp.use(express.json())registered on line 9, what willreq.bodyequal insidehandler, and what one-line fix corrects it? - Two middleware functions,
logRequestandcheckAuth, are registered withapp.use(checkAuth)on line 1 andapp.use(logRequest)on line 2. In what order do their console logs print for every incoming request, and does swapping the two lines change that order? - Why does Express require an error-handling middleware to have exactly four parameters,
(err, req, res, next), rather than letting you write a normal three-parameter function and call it an error handler by naming convention? - A route-specific middleware
checkAdminis passed asapp.get('/panel', checkAdmin, handler). IfcheckAdmincallsres.status(403).send('Denied')instead of callingnext(), doeshandlerever run for that request? Justify your answer using whatnext()actually does.
Summary
Middleware in Express is simply a JavaScript function with the signature (req, res, next) that sits somewhere between the incoming request and the final route handler. It can inspect or modify the shared req and res objects, and it must do exactly one of two things: call next() to pass the request forward to whatever is registered after it, or call a response method like res.send() to end the cycle right there — doing neither leaves the request hanging indefinitely. Middleware runs strictly in the order it is registered in your file, which is why placing express.json() after a route that reads req.body silently breaks that route, and why an authentication check must be registered before, not after, the route it is meant to protect. Middleware can be global (app.use(...), applying to every request), scoped to a single route (passed as an extra argument before the final handler), or a special four-parameter error handler that Express jumps to directly whenever any earlier function calls next(err). Together these pieces let a request pass through independent, reusable checkpoints — exactly like the sequence of checks a real IRCTC booking request passes through — before any actual work gets done.
Think About It
Think about this: How would you explain express middleware: processing requests to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.
Practice Exercises
Now it is time to practice! Complete these challenges to solidify your understanding:
- Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
- Exercise 2: Find a real-world example where express middleware: processing requests is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
- Exercise 3: Create a mind-map connecting express middleware: processing requests to at least 3 other topics you have studied.