New to Rust? Grab our free Rust for Beginners eBook Get it free →
How to write custom middleware for ExpressJS

A route can need the same check on every request, such as making sure a required header is present. In Express, middleware is a function that runs before the route handler and either sends a response or passes the request along.
The example writes a header check as middleware and attaches it to a single route.
What custom middleware does in an Express request
Middleware runs before the route handler, so it can inspect or change the request and either send a response or pass control onward.
Express calls the function with three arguments, and each one belongs to a different job.
| Argument | What it gives the middleware |
|---|---|
| req | The incoming request, including its method, path, and headers. |
| res | Methods for setting the status and sending the response. |
| next | A callback that continues the handler chain or passes an error to Express. |
What you need before writing the function
Use Node.js with npm and an Express project folder. If the folder does not have a package manifest yet, initialize one before installing Express.
- Create or open the project folder.
- Install Express from that folder.
npm init -y
npm install express
The second command adds Express to the project so Node can load it from the files below. The example uses CommonJS, which works in a project that has not enabled ECMAScript modules.
Write and mount a route-scoped middleware
The middleware trims the X-Client-Name header. It returns HTTP 400 for a blank value and stores a non-empty value on the request before calling next().
1. Validate the request header
The middleware reads the header before the route does any work, and an empty value must leave the request here. Return after sending HTTP 400 so the route callback cannot run.
function requireClientName(req, res, next) {
const clientName = req.get('X-Client-Name')?.trim();
if (!clientName) {
return res.status(400).json({ error: 'X-Client-Name is required' });
}
req.clientName = clientName;
next();
}
module.exports = requireClientName;
An empty value ends the request with HTTP 400. The middleware stores a valid trimmed value on req first, so the greeting handler reads it after next() continues the chain.
2. Put the middleware before the route handler
Pass the middleware between the route path and the response handler, which limits the check to GET /greeting.
const express = require('express');
const requireClientName = require('./middleware');
function createApp() {
const app = express();
app.get('/greeting', requireClientName, (req, res) => {
res.json({ message: `Hello, ${req.clientName}` });
});
return app;
}
if (require.main === module) {
const port = Number(process.env.PORT) || 3000;
createApp().listen(port, () => {
console.log(`Express listening on port ${port}`);
});
}
module.exports = { createApp };
Express invokes requireClientName before the route callback, which can read the stored req property only after middleware calls next(). A rejection ends at the middleware.
3. Test both branches
The test starts the app on a temporary local port and sends requests with and without the header. It reads both JSON responses and closes the server in a finally block, so a failed fetch does not leave the port open.
const { createApp } = require('./app');
async function main() {
const server = createApp().listen(0, '127.0.0.1');
await new Promise((resolve) => server.once('listening', resolve));
const address = server.address();
const url = `http://127.0.0.1:${address.port}/greeting`;
try {
const missing = await fetch(url);
console.log(`Missing header: ${missing.status} ${JSON.stringify(await missing.json())}`);
const accepted = await fetch(url, {
headers: { 'X-Client-Name': 'Ada' },
});
console.log(`Valid header: ${accepted.status} ${JSON.stringify(await accepted.json())}`);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
The output’s 400 error identifies the rejected request, and the greeting shows that middleware passed the trimmed name to its handler.
Keep each middleware branch on one path
A middleware branch should either finish the response or pass control onward. Choose one outcome per condition because sending a response and calling next() send Express down different paths.
| Branch | What Express does next | Use it when |
|---|---|---|
| Call next() | Continues to the next middleware or route handler. | The response is still open and processing should continue. |
| Send a response | Ends the normal request-response cycle with that result. | The request must be rejected or answered here. |
| Call next(error) | Skips normal handlers and enters error handling. | The middleware cannot complete its work because of an error. |
Without either action, Express has no instruction to continue and no response to send, so the request waits.
Error-handling middleware takes four arguments, with err first, and belongs after routes that can pass an error onward. The Express error-handling guide covers callback errors that middleware passes to next(error).
An X-Client-Name value is client-supplied input, so it cannot identify a user on its own. If a route needs a user session, CodeForGeek’s session-handling example covers that separate task.
Let each request leave one clear handoff
Each branch should end by sending a response or passing control with next(). Run node test.js after changing the validation rule and verify that rejected and accepted input keep their intended responses.
node test.js

Custom Express middleware FAQ
Route middleware can skip the remaining handlers for its route, and Express 5 also defines how returned promise errors reach error handling.
What does next(‘route’) do?
When route middleware is registered with app.get() or router.get(), calling next(‘route’) skips the remaining handlers for that route and lets Express check the next matching route. The ordinary next() callback continues to the next handler in the current chain.
Does Express 5 forward rejected promises from middleware?
Yes. When middleware returns a rejected promise or throws, Express 5 forwards that error to error handling. Callback-based work still needs its error passed to next(error).




