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.

ArgumentWhat it gives the middleware
reqThe incoming request, including its method, path, and headers.
resMethods for setting the status and sending the response.
nextA 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.

  1. Create or open the project folder.
  2. 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.

BranchWhat Express does nextUse it when
Call next()Continues to the next middleware or route handler.The response is still open and processing should continue.
Send a responseEnds 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
Terminal output showing missing-header rejection and valid-header response
The test receives HTTP 400 without the header and HTTP 200 when the route receives a valid value.

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).

Pankaj Kumar
Pankaj Kumar

Pankaj Kumar is the founder and CEO of CodeForGeek, with more than 14 years in IT. He is an open-source enthusiast who enjoys sharing what he learns through CodeForGeek and YouTube, with a focus on Python, data analytics, machine learning, Angular, Node.js, and Kafka.

Articles: 337