Custom Middleware
Objectives
By the end of this lesson, you should be able to:
- Write a custom middleware function that inspects or rejects a request
- Apply middleware to a single route instead of the whole app
- Decide between calling
next()and sending a response directly
💡 Why this matters: Built-in and third-party middleware (Lessons 2, 4-8) cover common needs, but application-specific logic, checking an API key, validating something about a request, is usually a custom middleware function written for exactly one project.
⚠️ A note on verification: every snippet and every response shown below was actually run and tested with real HTTP requests.
A Middleware That Can Reject a Request
const express = require('express');
const app = express();
function requireApiKey(req, res, next) {
const key = req.get('X-API-Key');
if (key !== 'secret123') {
return res.status(401).json({ error: 'Invalid or missing API key' });
}
next();
}
app.get('/public', (req, res) => {
res.json({ message: 'anyone can see this' });
});
app.get('/private', requireApiKey, (req, res) => {
res.json({ message: 'only with a valid key' });
});
app.listen(4202);
curl http://localhost:4202/public
{"message":"anyone can see this"}
curl -w " [%{http_code}]" http://localhost:4202/private
{"error":"Invalid or missing API key"} [401]
curl -w " [%{http_code}]" -H "X-API-Key: secret123" http://localhost:4202/private
{"message":"only with a valid key"} [200]
requireApiKey either sends a 401 response directly (and doesn’t call next(), since the request should stop here, never reaching the route handler) or calls next() to let a valid request continue. The return before res.status(401).json(...) matters (Module 5), without it, execution would fall through and call next() anyway, letting an invalid request through.
Applying Middleware to a Single Route
app.get('/private', requireApiKey, (req, res) => {...}) passes requireApiKey as a second argument, between the path and the final handler, Express runs it only for requests to /private, not for /public or any other route. This is different from app.use(requireApiKey), which would apply it to every route in the app. A route can even take multiple middleware functions this way, app.get(path, middleware1, middleware2, handler), each calling next() in turn before the final handler runs.
When to Call next() vs Send a Response
A middleware function does exactly one of two things: call next() to continue the pipeline (an authentication check that passed, a logger that just records something), or send a response and stop (an authentication check that failed, a validation error). Never do both, calling next() after already sending a response causes a real Express error (Cannot set headers after they are sent), since the request would be handled twice.
Try It
- Write a custom middleware
requireApiKey(or similar) and apply it to a single route using theapp.get(path, middleware, handler)pattern, confirming other routes are unaffected. - Write a second custom middleware that validates something about
req.body(for example, checking a required field exists) on aPOSTroute, sending a400if it’s missing. - Deliberately call both
next()andres.send()in the same middleware, run it, and observe the resulting Express error, then fix it by choosing one or the other. - Explain, in your own words, the difference between
app.use(middleware)andapp.get(path, middleware, handler).
Recap
- Custom middleware inspects a request, and either calls
next()to continue, or sends a response and stops, never both. - Passing middleware as an extra argument to
app.get/post/etc. applies it only to that specific route, not the whole app. - Custom middleware is where application-specific request-handling logic (authentication checks, validation) typically lives.
Next lesson: Morgan, logging every incoming request automatically.