Back to blog
Jun 27, 2025
4 min read

Creating Custom Middleware in Express.js for Cleaner Code

Custom middleware in Express.js helps modularize your code, enforce logic across routes, and improve maintainability. Learn how to build and apply your own middleware functions effectively.

Creating Custom Middleware in Express.js for Cleaner Code

Express.js is a minimalist and flexible Node.js framework that powers a huge number of web applications and APIs. One of its most powerful features is middleware , functions that can intercept and process requests before they reach your route handlers.

Middleware can handle everything from authentication and logging to error handling and data validation. And while Express provides many built-in and third-party middleware options, knowing how to create your own custom middleware gives you the power to make your code cleaner, more modular, and easier to manage.

Let’s explore how custom middleware works, how to write it, and where it fits in a modern Express application.


What Is Middleware in Express?

Middleware functions in Express have access to the request, response, and next objects. They can:

  • Modify request or response objects
  • End the request-response cycle
  • Call next() to pass control to the next middleware

A basic middleware signature looks like:

function (req, res, next) {
// logic here
next(); // call next middleware or route
}

Middleware can be applied:

  • Globally (for all routes)
  • To specific routes
  • In custom modules or external files

Why Use Custom Middleware?

While third-party packages like morgan, cors, or helmet are useful, there are many cases where custom logic is required, such as:

  • Validating request payloads
  • Checking user permissions
  • Attaching metadata to the request
  • Pre-processing or transforming input
  • Logging user behavior in a specific format

Instead of repeating code across routes, custom middleware allows centralized logic and clean separation of concerns.


Creating Your First Custom Middleware

Let’s walk through a simple example: a logger that tracks every incoming request.

// logger.js
function requestLogger(req, res, next) {
console.log(`${req.method} ${req.url}`);
next();
}

module.exports = requestLogger;

Now use it in your main server.js or app.js:

const express = require('express');
const requestLogger = require('./logger');

const app = express();

app.use(requestLogger);

app.get('/', (req, res) => {
res.send('Hello, world!');
});

Every request now logs the method and URL.


Middleware with Parameters

Sometimes your middleware needs options, like a role-based access check.

function checkRole(role) {
return function (req, res, next) {
if (req.user && req.user.role === role) {
next();
} else {
res.status(403).json({ error: 'Forbidden' });
}
};
}

Apply it like this:

app.get('/admin', checkRole('admin'), (req, res) => {
res.send('Welcome, admin!');
});

This pattern is useful for middleware factories where you need dynamic behavior.


Error-Handling Middleware

Express lets you define middleware that specifically handles errors:

function errorHandler(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something went wrong!');
}

app.use(errorHandler);

This middleware should be added after all other routes and middleware, as it only runs when next(err) is called.


Organizing Middleware for Larger Apps

For better structure:

  • Place custom middleware in a /middleware folder
  • Group related logic (auth, logging, validation) into separate files
  • Create reusable libraries for multi-project use

Example structure:

/middleware
├── auth.js
├── logger.js
└── validate.js

Then import and apply as needed:

const { isAuthenticated } = require('./middleware/auth');
app.get('/profile', isAuthenticated, profileController);

Best Practices

  • Keep middleware pure: Avoid tightly coupling to business logic
  • Use next() consistently: Unless you end the response, always call it
  • Log meaningfully: Custom logs help with debugging and auditing
  • Limit synchronous blocking: Use async middleware with care and handle errors properly
  • Use express-async-handler for wrapping async functions to handle rejections

Conclusion

Custom middleware is one of the best ways to write modular, reusable, and maintainable Express.js code. Instead of stuffing logic into your routes, use middleware to manage concerns like security, logging, validation, and request preprocessing.

Once you get comfortable writing your own middleware functions, you’ll find them indispensable for keeping your server code elegant and scalable.


Disclaimer

Article written with the help of AI.

Read the full Disclaimer HERE