What Middleware Really Is
Middleware is just a function that runs between request and response.
Think of your backend like an assembly line:
Request → Middleware → Middleware → Route → Response
/\* IMAGE PLACEHOLDER */
/\* ANIMATION PLACEHOLDER */
If routing is where things go, middleware is what happens along the way.
This is the moment Express usually “clicks”.
The Middleware Signature
Every middleware looks like this:
(req, res, next) => {
next();
};
req→ incoming requestres→ outgoing responsenext()→ pass control to the next middleware
If you don’t call next() (or send a response), the request stops.
Built-In Middleware
Express ships with some common middleware.
JSON body parser
app.use(express.json());
Without this, req.body is undefined.
Serving static files
app.use(express.static("public"));
Now files in /public are accessible from the browser.
/\* EXERCISE \*/ Try removing express.json() and logging req.body
to see what happens.
Custom Middleware
Let’s create a simple request logger:
const logger = (req, res, next) => {
console.log(req.method, req.path);
next();
};
app.use(logger);
Now every request prints:
GET /users
POST /login
This middleware runs for all routes because it’s registered with
app.use.
Middleware order matters: top to bottom.
/\* IMAGE PLACEHOLDER */Auth Middleware (Protected Routes)
Auth is just middleware.
Example:
const auth = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: "Unauthorized" });
}
next();
};
Apply it to a route:
app.get("/profile", auth, (req, res) => {
res.json({ user: "authenticated" });
});
Mental model:
- middleware decides access
- routes handle business logic
Clean separation.
/\* EXERCISE \*/ Add auth to multiple routes and experiment with missing headers.
Error-Handling Middleware
Error middleware looks slightly different:
const errorHandler = (err, req, res, next) => {
res.status(500).json({ error: err.message });
};
app.use(errorHandler);
Notice the four parameters.
To trigger it:
throw new Error("Something broke");
Or:
next(new Error("Invalid request"));
Now all errors flow to one place.
This gives you: - consistent responses - cleaner routes - easier debugging
Centralized errors are a professional backend pattern.
/\* IMAGE PLACEHOLDER */Validation Middleware
Validation is also middleware:
const validateUser = (req, res, next) => {
if (!req.body.email) {
return res.status(400).json({ error: "Email required" });
}
next();
};
Attach it:
app.post("/users", validateUser, (req, res) => {
res.json({ created: true });
});
Each middleware handles one responsibility.
Interactive Exercise: Middleware Flow
Try this: - Add multiple middleware - Change their order - See how requests move through the pipeline
Mini Project
Build an API with:
1. Request Logger
Logs method + path for every request.
2. Auth Middleware
Protect at least one route.
3. Validation Middleware
Validate POST request body.
4. Centralized Error Handler
Catch all thrown errors in one place.
Suggested structure:
src/
middlewares/
auth.js
logger.js
validate.js
errorHandler.js
routes/
index.js
/\* PROJECT EXTENSION \*/ Add rate limiting or request timing as bonus middleware.
Goal: small files, clear responsibilities.
Key Takeaways
- Middleware is a pipeline, not magic
- Order matters
- Auth, logging, validation, errors are all the same pattern
- Routes handle business logic
- Middleware handles cross-cutting concerns
You now understand how real backends are structured.