Express Basics
The Middleware Mental Model
LinkedIn Hook
"Why does every Node.js backend tutorial start with
app.use(express.json())-- and what does that line actually do?"Most developers learn Express by copy-pasting boilerplate. They wire up routes, sprinkle in
app.use()calls, and pray nothing breaks. But when an interviewer asks "what is middleware, really?" -- the room goes silent.Here's the truth: Express is barely a framework. It's a thin wrapper around Node's built-in
httpmodule that adds one powerful idea -- a chain of functions that each get a turn handling the request before sending the response.That's it. That's the whole framework. Routes are middleware. Body parsers are middleware. Authentication is middleware. Error handlers are middleware. Once you internalize the chain, every Express question becomes trivial.
In Lesson 6.1, I break down the request lifecycle, show why
next()is the most important function in Express, and prove why order matters more than logic in middleware design.Read the full lesson -> [link]
#NodeJS #ExpressJS #BackendDevelopment #JavaScript #InterviewPrep
What You'll Learn
- What Express actually is -- a thin wrapper over Node's built-in
httpmodule - The middleware mental model: request enters, flows through a chain, response exits
- How
app.use()registers middleware that runs for every request - How
app.get/post/put/deleteregister route-specific middleware - The full request lifecycle from incoming socket to outgoing response
- Why
next()is the heartbeat of Express -- and what happens when you forget to call it - Why the order of
app.use()calls determines correctness - How to write a simple logging middleware from scratch
- A teaser of error-handling middleware (4-argument signature)
The Airport Security Analogy — Why Middleware Exists
Imagine you arrive at an airport with a suitcase. To board your plane, you walk through a series of stations in a fixed order. First, the check-in desk verifies your ticket. Next, security scans your bag. Then, passport control stamps your documents. Finally, the gate agent lets you onto the plane.
Each station is independent. Each one inspects you, modifies something (a stamp, a tag, a boarding pass), and then waves you through to the next station. If any station refuses -- security finds a banned item, passport control rejects your visa -- you never reach the gate. The chain stops right there.
That is exactly how Express middleware works. Each incoming HTTP request is a passenger. Each middleware function is a station. Express hands the request to the first station, which does its job and either calls next() (waves the passenger through) or sends a response (stops the chain). The request flows from station to station until something finally answers it.
The order of stations matters enormously. You cannot stamp a passport before checking the ticket. You cannot board a plane before clearing security. In Express, you cannot authenticate a user before parsing the JSON body that contains their token. Order is logic.
+---------------------------------------------------------------+
| EXPRESS REQUEST LIFECYCLE |
+---------------------------------------------------------------+
| |
| Incoming HTTP Request |
| | |
| v |
| +---------------+ |
| | Node http | (raw req, res objects) |
| | server | |
| +-------+-------+ |
| | |
| v |
| +---------------+ |
| | Express app | (wraps req/res, adds helpers) |
| +-------+-------+ |
| | |
| v |
| +---------------+ next() +---------------+ |
| | Middleware 1 | -----------> | Middleware 2 | |
| | (logger) | | (json parser)| |
| +---------------+ +-------+-------+ |
| | |
| next() v |
| +---------------+ |
| | Middleware 3 | |
| | (auth check) | |
| +-------+-------+ |
| | |
| next() v |
| +---------------+ |
| | Route handler| |
| | res.send(..) | |
| +-------+-------+ |
| | |
| v |
| Outgoing Response |
| |
+---------------------------------------------------------------+
Napkin AI Visual Prompt: "Dark gradient (#0a1a0a -> #0d2e16). Horizontal pipeline of 4 rounded boxes connected by amber (#ffb020) arrows labeled 'next()'. Boxes from left to right: 'Logger', 'Body Parser', 'Auth', 'Route Handler' -- all in Node green (#68a063) outlines with white monospace labels. A blue request envelope enters from far left, a green response envelope exits to the right. A red X mark on one arrow shows what happens if next() is not called. Title: 'The Middleware Chain'."
What Express Actually Is — A Thin Wrapper Over http
Before diving into middleware, you need to understand what Express is not. Express is not a separate server. Express is not a runtime. Express is barely even a framework. It is a JavaScript library that takes Node's built-in http module and adds two things: a routing API and a middleware chain.
Here is a Node.js HTTP server with no Express at all:
// raw-server.js
// Pure Node.js -- no dependencies, no framework
const http = require('http');
// Create a server with a single request handler
const server = http.createServer((req, res) => {
// We have to manually parse URLs, methods, bodies, headers...
if (req.url === '/' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from raw Node!');
} else {
res.writeHead(404);
res.end('Not Found');
}
});
// Bind the server to a TCP port and start listening
server.listen(3000, () => {
console.log('Raw server running on http://localhost:3000');
});
This works, but it scales poorly. Every new route needs an if branch. Every new feature (JSON parsing, cookies, authentication) means writing your own helper. After ten routes you have a 500-line function with no structure.
Express wraps this exact same http.createServer call and gives you a cleaner API on top. When you write app.listen(3000), Express internally calls http.createServer(app) and passes the resulting server's request events to its own middleware dispatcher. The req and res you receive in Express handlers are the same objects Node gives you -- Express just decorates them with extra methods like res.json(), res.send(), and req.params.
+---------------------------------------------------------------+
| EXPRESS = http MODULE + MIDDLEWARE CHAIN |
+---------------------------------------------------------------+
| |
| Node http module |
| +------------------------------+ |
| | createServer((req, res)=>{})| |
| +--------------+---------------+ |
| | |
| | Express hooks in here |
| v |
| +------------------------------+ |
| | Express dispatcher | |
| | - matches URL + method | |
| | - runs middleware in order | |
| | - calls next() between them | |
| +------------------------------+ |
| |
+---------------------------------------------------------------+
Hello World in Express
The classic starting point. Notice how short it is compared to the raw Node version above.
// hello.js
// Install first: npm install express
const express = require('express');
// Create an Express application instance
const app = express();
// Register a GET handler for the root URL
// The callback signature is (req, res) -- same objects Node provides
app.get('/', (req, res) => {
// res.send() is an Express helper that sets Content-Type
// and ends the response in one call
res.send('Hello from Express!');
});
// Start the underlying http server on port 3000
// app.listen() internally calls http.createServer(app).listen(3000)
app.listen(3000, () => {
console.log('Express server running on http://localhost:3000');
});
Run it with node hello.js and visit http://localhost:3000. Six meaningful lines and you have a working server with a routed handler. Compare that to the raw http version's if/else ladder.
But here's the crucial point: that single app.get('/', handler) call registered a middleware. The route handler is itself a middleware function -- it just happens to be the last one in the chain that actually sends a response.
The app.use() Pattern — Middleware for Every Request
app.use() is how you register middleware that runs for every incoming request, regardless of URL or method. It is the most fundamental Express method, and understanding it unlocks the entire framework.
// app-use-basic.js
const express = require('express');
const app = express();
// This middleware runs FIRST for every request
// Signature: (req, res, next) -- three arguments
app.use((req, res, next) => {
// Attach a custom property to the request object
// Downstream middleware and routes can read it
req.requestTime = Date.now();
// IMPORTANT: call next() to pass control to the next middleware
next();
});
// This middleware runs SECOND for every request
app.use((req, res, next) => {
// Read the property set by the previous middleware
console.log(`Request at ${req.requestTime} -> ${req.method} ${req.url}`);
next();
});
// This route handler runs LAST for GET /
app.get('/', (req, res) => {
// We can still read requestTime here -- it survived the chain
res.send(`Hello! This request started at ${req.requestTime}`);
});
app.listen(3000);
Three things to notice:
- Every middleware has the same signature:
(req, res, next). Three arguments, always in that order. - Each middleware can mutate
reqandres-- adding properties, setting headers, anything. The mutations persist through the rest of the chain because it is the same JavaScript object passed by reference. next()advances the chain. Without it, the request hangs forever (or until the client times out).
Route Methods — app.get, app.post, app.put, app.delete
While app.use() registers middleware for every request, route methods register middleware for specific HTTP verbs and paths. Internally, they are still middleware -- Express just checks the verb and URL pattern before running them.
// route-methods.js
const express = require('express');
const app = express();
// Built-in middleware that parses JSON request bodies
// Without this, req.body would be undefined for POST/PUT requests
app.use(express.json());
// In-memory data store for demonstration
const users = [];
// GET /users -- read all users
app.get('/users', (req, res) => {
res.json(users);
});
// POST /users -- create a new user
// req.body is populated because we used express.json() above
app.post('/users', (req, res) => {
const newUser = { id: users.length + 1, ...req.body };
users.push(newUser);
// 201 Created is the correct status for successful resource creation
res.status(201).json(newUser);
});
// PUT /users/:id -- update a user (full replacement)
// :id is a route parameter, accessible via req.params.id
app.put('/users/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
const index = users.findIndex(u => u.id === id);
if (index === -1) return res.status(404).send('Not Found');
users[index] = { id, ...req.body };
res.json(users[index]);
});
// DELETE /users/:id -- remove a user
app.delete('/users/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
const index = users.findIndex(u => u.id === id);
if (index === -1) return res.status(404).send('Not Found');
const removed = users.splice(index, 1);
res.json(removed[0]);
});
app.listen(3000, () => {
console.log('CRUD server running on http://localhost:3000');
});
Each route method takes a path and one or more handler functions. You can pass multiple handlers to a single route -- they form a mini middleware chain just for that route.
Writing a Logging Middleware From Scratch
The classic first middleware everyone writes. It logs every incoming request along with its method, URL, and how long it took to process. This single example demonstrates the full power of the middleware chain.
// logger-middleware.js
const express = require('express');
const app = express();
// Custom logging middleware
// Define it as a regular function so we can reuse it across apps
function logger(req, res, next) {
// Capture the start time before passing control downstream
const start = Date.now();
// Log the incoming request immediately
console.log(`-> ${req.method} ${req.url}`);
// Hook into the response 'finish' event
// This fires AFTER the response has been fully sent to the client
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`<- ${req.method} ${req.url} ${res.statusCode} (${duration}ms)`);
});
// Pass control to the next middleware in the chain
// Without this line, no route handler ever runs
next();
}
// Register the logger globally -- runs for every request
app.use(logger);
app.get('/', (req, res) => {
res.send('Hello!');
});
app.get('/slow', (req, res) => {
// Simulate slow work to see the duration logged
setTimeout(() => res.send('Done'), 500);
});
app.listen(3000);
When you hit /slow, the console shows:
-> GET /slow
<- GET /slow 200 (502ms)
Notice how the logger ran before the route handler (the -> line) and after the response finished (the <- line). Middleware can wrap behavior around the rest of the chain just by hooking into events on res.
Why Order Matters — A Demonstration
Express runs middleware in the exact order you registered it. This is not a configuration. It is not a priority queue. It is a literal array, traversed top to bottom. Reordering two app.use() calls can completely change how your app behaves.
// order-matters.js
const express = require('express');
const app = express();
// CASE A: auth check BEFORE body parser
// This is BROKEN -- req.body is undefined when auth tries to read it
app.use((req, res, next) => {
// Trying to read req.body.token here -> TypeError: undefined
// because express.json() hasn't run yet
const token = req.body && req.body.token;
if (!token) return res.status(401).send('Unauthorized');
next();
});
// Body parser registered AFTER auth -- too late!
app.use(express.json());
app.post('/login', (req, res) => {
res.send(`Welcome, token holder`);
});
The fix is simple: swap the two app.use() calls so the body parser runs first.
// order-fixed.js
const express = require('express');
const app = express();
// CASE B: body parser FIRST, then auth check
// This works -- by the time auth runs, req.body is populated
app.use(express.json());
app.use((req, res, next) => {
const token = req.body && req.body.token;
if (!token) return res.status(401).send('Unauthorized');
// Attach the authenticated user to req for downstream handlers
req.user = { token };
next();
});
app.post('/login', (req, res) => {
res.send(`Welcome, ${req.user.token}`);
});
app.listen(3000);
The rule of thumb is: register middleware in the order data needs to flow. Parsers first, security next, business logic last, error handlers at the very bottom.
Calling next() vs Not Calling It
This is the single most common Express bug. Forgetting to call next() causes requests to hang silently until the client times out -- no error, no log, just a frozen browser tab.
// next-or-not.js
const express = require('express');
const app = express();
// MIDDLEWARE A: calls next() -- chain continues
app.use((req, res, next) => {
console.log('A ran');
next(); // Pass control downstream
});
// MIDDLEWARE B: forgets to call next() AND does not send a response
// This is a BUG -- the request hangs forever
app.use((req, res, next) => {
console.log('B ran');
// Oops -- no next(), no res.send()
// The request will time out after ~120 seconds
});
// MIDDLEWARE C: never runs because B never called next()
app.use((req, res, next) => {
console.log('C ran -- you will never see this');
next();
});
app.get('/', (req, res) => {
res.send('You will never reach me either');
});
app.listen(3000);
There are exactly three things a middleware can do, and it must do at least one of them:
- Call
next()to pass control to the next middleware. - Call a response method like
res.send(),res.json(), orres.end()to terminate the chain and answer the client. - Call
next(error)with an argument to skip ahead to error-handling middleware (covered next).
If a middleware does none of these, the request hangs. If a middleware does both (sends a response AND calls next()), Express may try to send headers twice and throw Cannot set headers after they are sent.
Error-Handling Middleware — A Teaser
Express has one special middleware signature: four arguments instead of three. When Express sees a function with (err, req, res, next), it treats it as an error handler. These are skipped during normal flow and only invoked when something calls next(error).
// error-teaser.js
const express = require('express');
const app = express();
app.get('/boom', (req, res, next) => {
// Manually trigger an error by passing it to next()
// Express will skip ahead to the error-handling middleware
const err = new Error('Something exploded');
err.status = 500;
next(err);
});
// Error-handling middleware -- note the FOUR arguments
// Always register error handlers LAST, after all routes and middleware
app.use((err, req, res, next) => {
console.error('Error caught:', err.message);
res.status(err.status || 500).json({
error: err.message,
});
});
app.listen(3000);
We will go deep on error handling, async errors, and the four-argument quirk in Lesson 6.2. For now, just remember: error middleware has four parameters and lives at the bottom of your chain.
Common Mistakes
1. Forgetting to call next() in custom middleware.
The number-one Express bug. Your middleware does its work, but you forget the next() call at the end. The request hangs silently. Always check the last line of every middleware -- it should be either next() or a response method like res.send().
2. Calling next() AND sending a response.
The opposite mistake. After sending a response with res.send(), you accidentally also call next(), which lets a downstream middleware try to send another response. Express throws Cannot set headers after they are sent to the client. Use return res.send(...) to make the early exit explicit.
3. Registering body parser AFTER routes that need req.body.
app.use(express.json()) must come before any route that reads req.body. If you register it after, req.body is undefined and your POST/PUT handlers crash. Always register parsers at the very top of your middleware stack.
4. Putting error-handling middleware in the wrong place. Error handlers must be registered after all routes and regular middleware. Express only routes errors to handlers that come later in the chain. An error handler at the top of the file will never run.
5. Confusing app.use(path, mw) with app.get(path, mw).
app.use('/api', mw) runs mw for any request whose URL starts with /api, regardless of method. app.get('/api', mw) runs mw only for GET /api exactly. Mixing these up causes mysterious "middleware doesn't run" or "middleware runs too often" bugs.
Interview Questions
1. "What is Express.js, and how does it relate to Node's built-in http module?"
Express is a minimal web framework for Node.js that wraps the built-in http module. When you call app.listen(), Express internally creates an HTTP server using http.createServer(app) and passes itself as the request handler. The req and res objects you receive in Express handlers are the same IncomingMessage and ServerResponse objects Node provides -- Express just decorates them with helper methods like res.json(), res.send(), req.params, and req.query. Express adds two main capabilities on top of http: a routing API for matching URLs and HTTP verbs to handlers, and a middleware chain that lets you compose request processing as a sequence of small functions. Without Express, you would manually parse URLs, methods, and bodies inside one giant request callback.
2. "Explain middleware in Express. What is the signature, and what are the three things a middleware can do?"
A middleware is a function with the signature (req, res, next) that sits between an incoming request and the final response. Express maintains an ordered list of registered middleware and runs them one at a time for each request. Each middleware can do exactly three things, and must do at least one of them: call next() to pass control to the next middleware in the chain; call a response method like res.send(), res.json(), or res.end() to terminate the chain and respond to the client; or call next(error) with an argument to skip ahead to error-handling middleware. If a middleware does none of these, the request hangs forever. Error-handling middleware is a special case -- it has four arguments (err, req, res, next) and only runs when an error has been passed via next(error).
3. "Why does the order of app.use() calls matter in Express?"
Express runs middleware in the literal order you registered it -- it is not a priority queue or a configuration map, it is a plain array traversed top to bottom. This means the order determines correctness. For example, express.json() must be registered before any route that reads req.body, because it is the middleware that parses the raw request body into a JavaScript object. Authentication middleware must come after body parsers if it reads tokens from the body, but before route handlers that need the authenticated user. Error-handling middleware must come last so it can catch errors from anything above it. The general rule is: parsers first, security and authentication next, business routes after that, and error handlers at the very bottom.
4. "What happens if you forget to call next() inside a middleware?"
The request hangs. Express never advances to the next middleware in the chain, no route handler ever runs, and no response is ever sent. From the client's perspective, the connection stays open until either the client times out (typically 30-120 seconds) or the server is restarted. There is no error message and no log entry -- the silence is the bug. The fix is to always end every middleware with either next() (to continue), a response method like res.send() (to terminate and answer), or next(error) (to jump to error handling). A useful habit is to write return res.send(...) instead of just res.send(...), which makes it visually obvious that the function exits at that point and prevents accidentally calling next() afterward.
5. "What is the difference between app.use() and app.get()?"
app.use() registers middleware that runs for every request matching the optional path prefix, regardless of HTTP method. app.use('/api', mw) runs mw for any URL starting with /api -- GET, POST, PUT, DELETE, anything. app.get() registers middleware that runs only for GET requests matching the exact path pattern (with route parameters). app.get('/users/:id', handler) runs only for GET /users/123 or similar, not POST /users/123. Internally, both register middleware in the same chain -- app.get() just adds a method check before invoking the handler. You use app.use() for cross-cutting concerns like logging, body parsing, CORS, and authentication. You use app.get/post/put/delete for actual API endpoints that respond to specific verbs.
Quick Reference — Express Basics Cheat Sheet
+---------------------------------------------------------------+
| EXPRESS BASICS CHEAT SHEET |
+---------------------------------------------------------------+
| |
| CREATE APP: |
| const express = require('express') |
| const app = express() |
| app.listen(3000) |
| |
| MIDDLEWARE SIGNATURE: |
| function mw(req, res, next) { ... next() } |
| |
| ERROR MIDDLEWARE (4 args): |
| function err(err, req, res, next) { ... } |
| |
| REGISTER GLOBAL MIDDLEWARE: |
| app.use(mw) |
| app.use('/api', mw) // path prefix |
| |
| REGISTER ROUTES: |
| app.get('/path', handler) |
| app.post('/path', handler) |
| app.put('/path/:id', handler) |
| app.delete('/path/:id', handler) |
| |
| BUILT-IN PARSERS: |
| app.use(express.json()) |
| app.use(express.urlencoded({ extended: true })) |
| |
+---------------------------------------------------------------+
+---------------------------------------------------------------+
| THE THREE MIDDLEWARE OUTCOMES |
+---------------------------------------------------------------+
| |
| 1. next() -> pass control to next middleware |
| 2. res.send(...) -> terminate chain, respond to client |
| 3. next(error) -> jump to error-handling middleware |
| |
| Doing none of these -> request hangs forever |
| Doing both 1 and 2 -> "headers already sent" error |
| |
+---------------------------------------------------------------+
+---------------------------------------------------------------+
| CORRECT MIDDLEWARE ORDER |
+---------------------------------------------------------------+
| |
| 1. Logging (express morgan / custom logger) |
| 2. Security (helmet, cors) |
| 3. Body parsers (express.json, express.urlencoded) |
| 4. Cookies/Session (cookie-parser, express-session) |
| 5. Authentication (passport, custom auth) |
| 6. Routes (app.get, app.post, ...) |
| 7. 404 handler (app.use catch-all) |
| 8. Error handler (4-arg middleware, very last) |
| |
+---------------------------------------------------------------+
| Concept | What It Means |
|---|---|
| Express | Thin wrapper around Node's http module + middleware chain |
| Middleware | Function (req, res, next) that processes a request |
app.use(mw) | Register mw for every request (optionally by path prefix) |
app.get(p, h) | Register h only for GET requests matching path p |
next() | Pass control to the next middleware in the chain |
next(err) | Skip ahead to error-handling middleware |
| Order | Literal registration order -- top to bottom of source file |
| Error handler | 4-argument middleware (err, req, res, next) registered last |
Prev: Lesson 5.4 -- Worker Threads Next: Lesson 6.2 -- Middleware Deep Dive
This is Lesson 6.1 of the Node.js Interview Prep Course -- 10 chapters, 42 lessons.