How to Use Express Middleware

How to Use Express Middleware Express.js is one of the most popular Node.js frameworks for building web applications and APIs. At the heart of its flexibility and power lies a core concept known as middleware . Middleware functions are essential components that sit between the incoming request and the final response, allowing developers to modify, inspect, or terminate requests and responses befor

Nov 6, 2025 - 11:14
Nov 6, 2025 - 11:14
 1

How to Use Express Middleware

Express.js is one of the most popular Node.js frameworks for building web applications and APIs. At the heart of its flexibility and power lies a core concept known as middleware. Middleware functions are essential components that sit between the incoming request and the final response, allowing developers to modify, inspect, or terminate requests and responses before they reach their final destination. Whether you're logging requests, authenticating users, parsing JSON, or serving static files, Express middleware provides a clean, modular, and scalable way to handle these tasks.

Understanding how to use Express middleware effectively is not just a technical skillits a foundational requirement for building robust, maintainable, and secure web applications. Many developers new to Express struggle with middleware because its behavior can seem abstract or non-linear. But once you grasp how middleware functions are chained, executed, and controlled, you unlock the ability to create highly organized, reusable, and efficient application logic.

This guide will walk you through everything you need to know about Express middlewarefrom the basics of how it works to advanced patterns and real-world implementations. By the end, youll be able to write, organize, and debug middleware with confidence, applying industry best practices to your own projects.

Step-by-Step Guide

Understanding the Middleware Function Signature

At its core, an Express middleware function is a JavaScript function that has access to the request object (req), the response object (res), and the next middleware function in the applications request-response cycle (next). The signature looks like this:

function(req, res, next) {

// Your logic here

next(); // Pass control to the next middleware

}

The next parameter is critical. If you forget to call it, the request will hang indefinitely, and your application will appear unresponsive. This is one of the most common mistakes made by beginners.

Middleware functions can perform the following tasks:

  • Execute any code
  • Modify the request and response objects
  • End the request-response cycle
  • Call the next middleware function in the stack

Middleware can be loaded at the application level or the router level, giving you fine-grained control over where and how its applied.

Setting Up Your Express Application

To begin using middleware, you first need a basic Express application. If you havent already set one up, create a new directory and initialize a Node.js project:

mkdir express-middleware-demo

cd express-middleware-demo

npm init -y

npm install express

Then, create a file named app.js and add the following minimal setup:

const express = require('express');

const app = express();

const PORT = 3000;

app.listen(PORT, () => {

console.log(Server running on http://localhost:${PORT});

});

This creates a basic Express server. Now, well start adding middleware.

Application-Level Middleware

Application-level middleware is bound to the entire application using app.use() or app.METHOD(), where METHOD is an HTTP verb like get, post, etc.

Lets create a simple logging middleware that records every incoming request:

const express = require('express');

const app = express();

const PORT = 3000;

// Application-level middleware

app.use((req, res, next) => {

console.log(Time: ${new Date().toISOString()}, Method: ${req.method}, URL: ${req.url});

next();

});

app.get('/', (req, res) => {

res.send('Hello World!');

});

app.listen(PORT, () => {

console.log(Server running on http://localhost:${PORT});

});

When you start the server and visit http://localhost:3000, youll see the log output in your terminal. The middleware runs for every request, regardless of the route, because we used app.use() without a path.

You can also restrict middleware to specific paths:

app.use('/api', (req, res, next) => {

console.log('API request received');

next();

});

In this case, the middleware only executes for routes that begin with /api.

Router-Level Middleware

Router-level middleware works the same way as application-level middleware, but it is bound to an instance of the express.Router() object. This is ideal for modularizing your application, especially when building APIs with multiple endpoints.

Create a new file called routes/user.js:

const express = require('express');

const router = express.Router();

// Middleware specific to this router

router.use((req, res, next) => {

console.log('User route accessed');

next();

});

router.get('/', (req, res) => {

res.json({ message: 'List of users' });

});

router.get('/:id', (req, res) => {

res.json({ message: User with ID ${req.params.id} });

});

module.exports = router;

Then, in your main app.js, import and use the router:

const express = require('express');

const userRouter = require('./routes/user');

const app = express();

const PORT = 3000;

app.use('/users', userRouter);

app.listen(PORT, () => {

console.log(Server running on http://localhost:${PORT});

});

Now, any request to /users or /users/:id will trigger the router-level middleware. This keeps your code organized and scalable.

Middleware for Request Parsing

Express does not parse request bodies by default. To handle JSON or URL-encoded data, you must use built-in middleware:

app.use(express.json()); // Parses JSON bodies

app.use(express.urlencoded({ extended: true })); // Parses URL-encoded bodies

These should be placed early in your middleware stackbefore any route handlers that expect to read req.body.

Example with a POST route:

app.use(express.json());

app.use(express.urlencoded({ extended: true }));

app.post('/users', (req, res) => {

console.log(req.body); // Now accessible

res.json({ received: req.body });

});

Without these middleware functions, req.body will be undefined, leading to runtime errors.

Handling Errors with Error-Handling Middleware

Express has a special type of middleware for handling errors: error-handling middleware. It has four parameters instead of three: (err, req, res, next).

Example of a custom error handler:

app.use((err, req, res, next) => {

console.error(err.stack);

res.status(500).send('Something broke!');

});

To trigger this, you can throw an error in any route:

app.get('/error', (req, res, next) => {

throw new Error('Something went wrong!');

});

Important: Error-handling middleware must be defined after all other middleware and routes. If placed before, it wont catch errors from subsequent routes.

You can also create more sophisticated error handlers:

app.use((err, req, res, next) => {

const statusCode = err.statusCode || 500;

const message = err.message || 'Internal Server Error';

res.status(statusCode).json({

error: {

message,

stack: process.env.NODE_ENV === 'development' ? err.stack : {}

}

});

});

This provides better feedback in development while hiding sensitive stack traces in production.

Creating Custom Middleware Functions

Custom middleware functions improve code reusability and readability. Instead of writing logic inline, extract it into named functions.

Example: Authentication middleware

function authenticateToken(req, res, next) {

const token = req.headers['authorization'];

if (!token) {

return res.status(401).json({ error: 'Access token required' });

}

// Simulate token verification

if (token === 'secret-token-123') {

req.user = { id: 1, name: 'John Doe' };

next();

} else {

res.status(403).json({ error: 'Invalid token' });

}

}

app.get('/profile', authenticateToken, (req, res) => {

res.json({ user: req.user });

});

Now, any route that requires authentication can simply include authenticateToken as a parameter. You can even chain multiple middleware functions:

app.get('/profile', authenticateToken, checkRole, (req, res) => {

res.json({ user: req.user });

});

This modular approach makes your code easier to test and maintain.

Using Third-Party Middleware

Express has a rich ecosystem of third-party middleware packages. Some of the most popular include:

  • cors Enables Cross-Origin Resource Sharing
  • helmet Secures your app with HTTP headers
  • morgan HTTP request logger
  • express-rate-limit Prevents brute-force attacks

Install and use them like this:

npm install cors helmet morgan express-rate-limit

const cors = require('cors');

const helmet = require('helmet');

const morgan = require('morgan');

const rateLimit = require('express-rate-limit');

app.use(helmet()); // Security headers

app.use(cors()); // Allow cross-origin requests

app.use(morgan('dev')); // Log requests

app.use(rateLimit({

windowMs: 15 * 60 * 1000, // 15 minutes

max: 100 // limit each IP to 100 requests per windowMs

}));

These tools add enterprise-grade security and observability with minimal code.

Order Matters: The Middleware Stack

Middleware functions are executed in the order they are defined. This is crucial to understand.

Consider this example:

app.use((req, res, next) => {

res.send('I stopped the request!');

});

app.use(express.json());

app.get('/', (req, res) => {

res.send('Hello World');

});

Here, the first middleware sends a response immediately and never calls next(). As a result, express.json() and the GET route are never executed. The server will respond with "I stopped the request!" for every request.

Best practice: Always place middleware that modifies the request (like parsing or authentication) before routes that depend on them. Place error-handling middleware at the end.

Skipping Middleware with next('route')

By default, calling next() moves to the next middleware function in the stack. However, if you're using middleware within a route definition (not app.use()), you can skip to the next route handler using next('route').

Example:

app.get('/user/:id', (req, res, next) => {

if (req.params.id === '0') {

next('route'); // Skip to next route handler

} else {

next(); // Continue to next middleware

}

}, (req, res) => {

res.send('User ID is not zero');

});

app.get('/user/:id', (req, res) => {

res.send('User ID is zero');

});

In this case, if the ID is 0, the first route handler skips to the second one. This is useful for conditional routing logic without duplicating routes.

Best Practices

Keep Middleware Focused and Single-Purpose

Each middleware function should do one thing well. Avoid creating god middleware that handles authentication, logging, validation, and error handling all at once. Instead, break it into smaller, reusable functions:

  • logRequest()
  • authenticateUser()
  • validateEmail()
  • handleErrors()

This improves testability, readability, and maintainability. You can easily swap out or disable individual components without affecting others.

Use Middleware for Cross-Cutting Concerns

Middleware is ideal for cross-cutting concernsfeatures that span multiple parts of your application. These include:

  • Request logging
  • Authentication and authorization
  • Rate limiting
  • Input validation
  • Response formatting
  • Security headers

By centralizing these in middleware, you avoid code duplication and ensure consistent behavior across all routes.

Always Call next() Unless Intentionally Ending the Response

One of the most common bugs in Express apps is forgetting to call next(). If you intend to pass control to the next middleware, always call it. If youre sending a response, dont call next()youve already completed the cycle.

Bad:

app.use((req, res, next) => {

if (!req.headers.authorization) {

res.status(401).send('Unauthorized');

// Missing next() this is okay because we sent a response

}

// But if we don't return here, next() will still be called after sending!

next(); // ? This causes an error: Can't set headers after they are sent

});

Good:

app.use((req, res, next) => {

if (!req.headers.authorization) {

return res.status(401).send('Unauthorized'); // ? Return after sending

}

next(); // ? Only call next if we didn't respond

});

Always use return after sending a response to prevent accidental multiple responses.

Organize Middleware by Layer

Structure your middleware in a logical order:

  1. Security middleware (helmet, cors)
  2. Request parsing (express.json(), express.urlencoded())
  3. Logging (morgan)
  4. Authentication and authorization
  5. Custom business logic
  6. Routes
  7. Error-handling middleware

This order ensures security and parsing are handled before any route logic, and errors are caught at the end.

Use Environment-Specific Middleware

Some middleware should only run in development (like verbose logging) or production (like rate limiting). Use environment variables to conditionally apply them:

if (process.env.NODE_ENV === 'development') {

app.use(morgan('dev'));

}

if (process.env.NODE_ENV === 'production') {

app.use(rateLimit({

windowMs: 15 * 60 * 1000,

max: 100

}));

}

This keeps your production environment lean and secure while providing useful debugging tools during development.

Write Unit Tests for Your Middleware

Since middleware functions are pure JavaScript functions, they are easy to test in isolation. Use a testing framework like Jest or Mocha to verify their behavior.

Example test for authentication middleware:

const request = require('supertest');

const app = require('../app');

describe('authenticateToken middleware', () => {

it('should reject requests without token', async () => {

const res = await request(app).get('/profile');

expect(res.status).toBe(401);

expect(res.body).toEqual({ error: 'Access token required' });

});

it('should allow requests with valid token', async () => {

const res = await request(app)

.get('/profile')

.set('Authorization', 'secret-token-123');

expect(res.status).toBe(200);

expect(res.body.user.name).toBe('John Doe');

});

});

Testing middleware ensures reliability and reduces regressions as your application grows.

Avoid Blocking Operations in Middleware

Middleware functions should be fast. Avoid synchronous blocking operations like reading large files or complex database queries directly in middleware. Use asynchronous patterns instead:

app.use(async (req, res, next) => {

try {

const user = await User.findById(req.headers['user-id']);

req.user = user;

next();

} catch (err) {

next(err);

}

});

Always wrap async middleware in try-catch blocks and pass errors to next() to ensure theyre handled by your error-handling middleware.

Tools and Resources

Essential npm Packages

Here are the most valuable middleware packages for Express applications:

  • cors Enables CORS for cross-domain requests.
  • helmet Protects against common web vulnerabilities by setting HTTP headers.
  • morgan HTTP request logger with customizable formats.
  • express-rate-limit Limits repeated requests from the same IP to prevent abuse.
  • express-validator Validates and sanitizes request data with a rich set of validators.
  • express-session Manages user sessions with cookies and memory or Redis storage.
  • jsonwebtoken Generates and verifies JSON Web Tokens for stateless authentication.

Install these with:

npm install cors helmet morgan express-rate-limit express-validator express-session jsonwebtoken

Development Tools

  • Postman Test API endpoints and simulate headers, body, and authentication.
  • Insomnia Open-source alternative to Postman with excellent environment management.
  • nodemon Automatically restarts your server on file changes during development: npm install -g nodemon
  • Express.js Debugger Use the DEBUG environment variable: DEBUG=express:* node app.js

Documentation and Learning Resources

Monitoring and Logging

For production applications, consider integrating middleware with logging platforms:

  • Winston Flexible logging library with file, console, and transport support.
  • Loggly Cloud-based log management with search and alerting.
  • Datadog Full-stack monitoring with request tracing and performance metrics.

These tools help you understand traffic patterns, detect anomalies, and debug issues in real time.

Real Examples

Example 1: Secure API with Authentication and Validation

Lets build a complete example that combines multiple middleware functions into a secure user API.

File: routes/user.js

const express = require('express');

const { body, validationResult } = require('express-validator');

const router = express.Router();

// Middleware: Validate email and password

const validateUser = [

body('email').isEmail().withMessage('Valid email required'),

body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters'),

(req, res, next) => {

const errors = validationResult(req);

if (!errors.isEmpty()) {

return res.status(400).json({ errors: errors.array() });

}

next();

}

];

// Middleware: Mock authentication

const authenticate = (req, res, next) => {

const token = req.headers['authorization'];

if (token === 'valid-token') {

req.user = { id: 1, email: 'user@example.com' };

next();

} else {

res.status(401).json({ error: 'Invalid or missing token' });

}

};

// POST /users - Create user (requires auth and validation)

router.post('/', authenticate, validateUser, (req, res) => {

res.status(201).json({

message: 'User created',

user: req.user

});

});

// GET /users/me - Get current user (requires auth)

router.get('/me', authenticate, (req, res) => {

res.json({ user: req.user });

});

module.exports = router;

File: app.js

const express = require('express');

const userRouter = require('./routes/user');

const app = express();

const PORT = 3000;

// Security and parsing

app.use(express.json());

app.use(express.urlencoded({ extended: true }));

app.use(require('helmet')());

// Routes

app.use('/api/users', userRouter);

// Error handling

app.use((err, req, res, next) => {

console.error(err.stack);

res.status(500).json({ error: 'Something went wrong!' });

});

app.listen(PORT, () => {

console.log(API running on http://localhost:${PORT}/api/users);

});

Now test with Postman:

  • POST http://localhost:3000/api/users with headers: Authorization: valid-token and body: { "email": "test@example.com", "password": "123456" } ? 201 Created
  • POST without token ? 401 Unauthorized
  • POST with invalid email ? 400 with validation errors

Example 2: Rate-Limited Public API

Many public APIs need to limit usage to prevent abuse. Heres how to apply rate limiting to specific routes:

const rateLimit = require('express-rate-limit');

// Create a limiter for public endpoints

const publicLimiter = rateLimit({

windowMs: 1 * 60 * 1000, // 1 minute

max: 5, // limit each IP to 5 requests per windowMs

message: { error: 'Too many requests, please try again later.' }

});

// Apply to public routes only

app.use('/public', publicLimiter);

app.get('/public/data', (req, res) => {

res.json({ data: 'public info' });

});

app.get('/admin/data', (req, res) => {

res.json({ data: 'admin info' }); // No rate limit

});

Now, the /public/data endpoint is protected, while /admin/data remains unrestricted.

Example 3: Dynamic Middleware Based on Role

Lets create a role-based access control (RBAC) system:

function roleRequired(role) {

return (req, res, next) => {

if (!req.user || req.user.role !== role) {

return res.status(403).json({ error: 'Forbidden' });

}

next();

};

}

app.get('/admin/dashboard', authenticate, roleRequired('admin'), (req, res) => {

res.json({ message: 'Admin dashboard' });

});

app.get('/moderator/dashboard', authenticate, roleRequired('moderator'), (req, res) => {

res.json({ message: 'Moderator dashboard' });

});

This pattern allows you to reuse the same middleware across multiple routes with different role requirements.

FAQs

What is the difference between app.use() and app.get() for middleware?

app.use() applies middleware to all HTTP methods for a given path. app.get(), app.post(), etc., apply middleware only to that specific HTTP method. For example, app.use('/api', logger) logs all requests to /api regardless of whether they are GET, POST, or DELETE. But app.get('/api', logger) only logs GET requests to /api.

Can middleware be asynchronous?

Yes, middleware can be asynchronous. However, you must handle errors properly. Wrap async middleware in try-catch blocks and call next(err) to pass errors to Expresss error-handling middleware. Never use await without a try-catch unless youre certain no errors will occur.

Why does my middleware not run on certain routes?

This usually happens when middleware is applied to a specific path and the route doesnt match. For example, if you use app.use('/admin', auth), it only runs for routes starting with /admin. Also, if a middleware calls res.send() or res.end(), it stops the chain and subsequent middleware wont run.

How do I test middleware without starting the server?

You can mock the req, res, and next objects and call the middleware function directly. Libraries like supertest make this easier, but you can also create simple mocks:

const mockReq = { headers: { authorization: 'valid-token' } };

const mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn() };

const mockNext = jest.fn();

yourMiddleware(mockReq, mockRes, mockNext);

expect(mockNext).toHaveBeenCalled();

Can I use middleware in Express 4 and Express 5?

Yes. The middleware API has remained consistent since Express 4. Express 5 (when released) will maintain backward compatibility. Always refer to the official Express documentation for version-specific changes.

Is middleware the same as a filter in other frameworks?

Yes. In other frameworks like Spring Boot (Java) or ASP.NET Core (C

), middleware is often called filters or interceptors. The concept is identical: intercept requests/responses to add cross-cutting logic before or after the main handler.

What happens if I call next() twice?

Calling next() twice will result in an error: Can't set headers after they are sent. Express throws this error because the response has already been sent, and a second call tries to modify it again. Always ensure next() is called only once per request, unless you're intentionally skipping routes with next('route').

Conclusion

Express middleware is one of the most powerful and flexible features of the Express.js framework. It enables developers to build clean, modular, and scalable applications by separating concerns into reusable, testable units. From basic request logging to complex authentication pipelines, middleware allows you to control the flow of data through your application with precision.

By following the best practices outlined in this guidekeeping middleware focused, ordering them correctly, testing them rigorously, and leveraging third-party toolsyoull avoid common pitfalls and build applications that are secure, performant, and maintainable.

Remember: middleware is not just a technical toolits a design philosophy. It encourages separation of concerns, composability, and reusability. As your application grows, the structure you build around middleware will determine how easily you can extend, debug, and scale your codebase.

Start small. Build one middleware function at a time. Test it. Refactor it. Then chain it with others. Over time, youll develop an intuitive sense for when and where to use middleware, transforming your Express applications from simple scripts into professional-grade services.

Now that you understand how to use Express middleware effectively, go build something great.