Skip to content

Setting Up Express & Middleware

Middleware functions are the backbone of Express applications. They have access to the request object, response object, and the next middleware function in the application’s request-response cycle. This guide covers best practices for organizing, implementing, and using middleware in Express applications.

Middleware functions can perform a variety of tasks:

  • Execute code during the request-response cycle
  • Modify request or response objects
  • End the request-response cycle
  • Call the next middleware in the stack
const express = require('express');
const app = express();
// Middleware 1
app.use((req, res, next) => {
console.log('Middleware 1 executed');
next(); // Pass control to the next middleware
});
// Middleware 2
app.use((req, res, next) => {
console.log('Middleware 2 executed');
next();
});
// Route handler
app.get('/', (req, res) => {
res.send('Hello World');
});
app.listen(3000);

We organize middleware in a dedicated /middlewares folder to keep the codebase clean and maintainable:

  • Directorysrc/
    • Directorymiddlewares/
      • authMiddleware.js
      • loggerMiddleware.js
      • errorMiddleware.js
      • validationMiddleware.js
      • corsMiddleware.js
      • notFoundMiddleware.js
    • Directoryroutes/
    • Directorycontrollers/
    • app.js
    • server.js

All middleware should be registered in app.js with consistent camelCase naming:

// app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const logger = require('./utils/logger');
const corsMiddleware = require('./middlewares/corsMiddleware');
const loggerMiddleware = require('./middlewares/loggerMiddleware');
const errorMiddleware = require('./middlewares/errorMiddleware');
const authMiddleware = require('./middlewares/authMiddleware');
const notFoundMiddleware = require('./middlewares/notFoundMiddleware');
const PORT = process.env.PORT || 3000;
const app = express();
// Third-party middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(corsMiddleware);
app.use(helmet()); // Directly applying helmet with default settings
// Logging middleware - choose one approach
// Option 1: Use morgan for HTTP request logging
// app.use(morgan('dev'));
// Option 2: Use custom logger middleware for more detailed logging
app.use(loggerMiddleware);
// Routes
app.use('/api/auth', require('./routes/auth.routes'));
app.use('/api/products', require('./routes/products.routes'));
app.use('/api/admin', require('./routes/admin.routes'));
// Not found middleware
app.use(notFoundMiddleware);
// Error middleware (always last)
app.use(errorMiddleware);
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});
module.exports = app;
// middlewares/corsMiddleware.js
const cors = require('cors');
const corsOptions = {
origin: function (origin, callback) {
// Allow requests with no origin (like mobile apps, curl)
const allowedOrigins = ['http://localhost:3000', 'https://yourdomain.com'];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
optionsSuccessStatus: 200,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization']
};
const corsMiddleware = cors(corsOptions);
module.exports = corsMiddleware;

What it does:

CORS (Cross-Origin Resource Sharing) middleware allows your Express API to handle requests from different origins (domains). It works by adding appropriate HTTP headers to the server’s responses, which tell browsers whether they should allow web applications running at different origins to access your API.

Key features:

  • Controls which domains can access your API
  • Configures allowed HTTP methods
  • Specifies allowed headers
  • Handles preflight requests
  • Enables credentials for cross-origin requests (cookies, authentication)

For specific middleware patterns, refer to their dedicated guides:

// middlewares/notFoundMiddleware.js
const { StatusCodes } = require('http-status-codes');
/**
* Not Found middleware
*
* This middleware handles requests to routes that don't exist.
* Should be placed after all route definitions and before the error middleware.
*/
const notFoundMiddleware = (req, res) => {
res.status(StatusCodes.NOT_FOUND).json({
statusCode: StatusCodes.NOT_FOUND,
message: 'Resource not found',
path: req.originalUrl,
timestamp: new Date().toISOString()
});
};
module.exports = notFoundMiddleware;

Sometimes middleware should only apply to specific routes:

// routes/admin.routes.js
const express = require('express');
const router = express.Router();
const { authMiddleware } = require('../middlewares/authMiddleware');
const { authorize } = require('../middlewares/rbac.middleware');
const adminController = require('../controllers/admin.controller');
// Apply middleware to all routes in this router
router.use(authMiddleware);
router.use(authorize('admin'));
// Admin routes
router.get('/users', adminController.getAllUsers);
router.delete('/users/:id', adminController.deleteUser);
module.exports = router;
// In app.js
app.use('/api/admin', require('./routes/admin.routes'));

Error handling middleware should always be registered last:

// app.js
const express = require('express');
const app = express();
const { errorMiddleware } = require('./middlewares/errorMiddleware');
// Regular middleware and routes...
// Error handling middleware (must be last)
app.use(errorMiddleware);
  1. Keep middleware focused

    • Each middleware should have a single responsibility
    • Create separate files for different middleware functions
  2. Order matters

    • Place middleware in the correct order based on dependencies
    • Error-handling middleware should always be last
  3. Use next() properly

    • Always call next() to pass control
    • Call next(err) to skip to error handlers
  4. Modularize your middleware

    • Export individual middleware functions
    • Group related middleware in the same file
  5. Handle middleware errors

    • Use try/catch blocks in async middleware
    • Or use express-async-errors package

Forgetting to call next()

  • Requests will hang if next() isn’t called in middleware that doesn’t end the response

Using middleware after sending a response

  • Once res.send()/res.json()/res.end() is called, subsequent middleware won’t execute

Incorrect middleware order

  • Body-parsing middleware must come before routes that need parsed body data
  • Authentication middleware should come before protected routes

Not handling async errors

  • Unhandled promise rejections can crash your application
  • Always use try/catch or a promise-catching utility

Middleware is a powerful feature of Express that allows you to insert processing logic into the request-response cycle. By organizing middleware in a dedicated folder and following best practices, you can create a clean, maintainable, and secure Express application.

When implementing middleware, remember:

  1. Keep middleware functions focused on a single responsibility
  2. Use the correct order of middleware registration
  3. Handle errors appropriately
  4. Modularize your middleware code
  5. Use built-in and third-party middleware wisely