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 Fundamentals
Section titled “Middleware Fundamentals”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
Middleware Execution Flow
Section titled “Middleware Execution Flow”const express = require('express');const app = express();
// Middleware 1app.use((req, res, next) => { console.log('Middleware 1 executed'); next(); // Pass control to the next middleware});
// Middleware 2app.use((req, res, next) => { console.log('Middleware 2 executed'); next();});
// Route handlerapp.get('/', (req, res) => { res.send('Hello World');});
app.listen(3000);
Organizing Middleware
Section titled “Organizing Middleware”Project Structure
Section titled “Project Structure”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
Middleware Registration
Section titled “Middleware Registration”All middleware should be registered in app.js
with consistent camelCase naming:
// app.jsconst 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 middlewareapp.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 loggingapp.use(loggerMiddleware);
// Routesapp.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 middlewareapp.use(notFoundMiddleware);
// Error middleware (always last)app.use(errorMiddleware);
app.listen(PORT, () => { logger.info(`Server running on port ${PORT}`);});
module.exports = app;
Common Middleware
Section titled “Common Middleware”Essential Third-Party Middleware
Section titled “Essential Third-Party Middleware”// middlewares/corsMiddleware.jsconst 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)
// middlewares/morganMiddleware.jsconst morgan = require('morgan');const logger = require('../utils/logger'); // Custom logger (e.g., winston)
// Create a custom token for request IDmorgan.token('id', function getId(req) { return req.id;});
// Define different formats for development and productionconst developmentFormat = ':method :url :status :response-time ms - :res[content-length] - :id';const productionFormat = ':id :method :url :status :response-time ms';
// Create a write stream for morganconst stream = { write: (message) => logger.info(message.trim())};
const morganMiddleware = process.env.NODE_ENV === 'production' ? morgan(productionFormat, { stream }) : morgan(developmentFormat, { stream });
module.exports = morganMiddleware;
What it does:
Morgan is an HTTP request logger middleware for Express that generates logs for every incoming request. It provides visibility into your API’s traffic and helps with debugging and monitoring.
Key features:
- Logs HTTP request details (method, URL, status code, response time)
- Supports customizable logging formats
- Can be integrated with other logging systems
- Helps track request performance
- Provides different preset formats (‘dev’, ‘combined’, ‘common’, etc.)
What it does:
Helmet helps secure Express apps by setting various HTTP security headers. These headers help protect your application from well-known web vulnerabilities like Cross-Site Scripting (XSS), clickjacking, and other security threats.
Key headers set by Helmet:
- Content-Security-Policy: Controls which resources the browser is allowed to load
- X-XSS-Protection: Enables browser’s built-in XSS filters
- X-Frame-Options: Prevents clickjacking by controlling frame embedding
- Strict-Transport-Security: Forces HTTPS connections
- X-Content-Type-Options: Prevents MIME-type sniffing
- Referrer-Policy: Controls how much referrer information is sent
- X-DNS-Prefetch-Control: Controls DNS prefetching
Custom Middleware Implementation
Section titled “Custom Middleware Implementation”For specific middleware patterns, refer to their dedicated guides:
- Authentication Middleware: See Authentication & Security guide
- Error Handling & Logging Middlewares: See Error Handling & Logging guide
// middlewares/notFoundMiddleware.jsconst { 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;
// middlewares/validationMiddleware.jsconst { StatusCodes } = require('http-status-codes');const { validationResult } = require('express-validator');
const validateRequest = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(StatusCodes.BAD_REQUEST).json({ statusCode: StatusCodes.BAD_REQUEST, message: 'Validation failed', errors: errors.array().map(error => ({ field: error.param, message: error.msg })), path: req.originalUrl, timestamp: new Date().toISOString() }); } next();};
module.exports = validateRequest;
Usage example with route:
// routes/users.routes.jsconst express = require('express');const { body } = require('express-validator');const { validateRequest } = require('../middlewares/validationMiddleware');const userController = require('../controllers/user.controller');
const router = express.Router();
router.post( '/register', [ body('email').isEmail().withMessage('Please provide a valid email'), body('password').isLength({ min: 8 }).withMessage('Password must be at least 8 characters'), body('name').notEmpty().withMessage('Name is required') ], validateRequest, userController.register);
module.exports = router;
Route-Specific Middleware
Section titled “Route-Specific Middleware”Sometimes middleware should only apply to specific routes:
// routes/admin.routes.jsconst 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 routerrouter.use(authMiddleware);router.use(authorize('admin'));
// Admin routesrouter.get('/users', adminController.getAllUsers);router.delete('/users/:id', adminController.deleteUser);
module.exports = router;
// In app.jsapp.use('/api/admin', require('./routes/admin.routes'));
Error Handling Middleware
Section titled “Error Handling Middleware”Error handling middleware should always be registered last:
// app.jsconst express = require('express');const app = express();const { errorMiddleware } = require('./middlewares/errorMiddleware');
// Regular middleware and routes...
// Error handling middleware (must be last)app.use(errorMiddleware);
Best Practices & Common Pitfalls
Section titled “Best Practices & Common Pitfalls”Best Practices
Section titled “Best Practices”-
Keep middleware focused
- Each middleware should have a single responsibility
- Create separate files for different middleware functions
-
Order matters
- Place middleware in the correct order based on dependencies
- Error-handling middleware should always be last
-
Use next() properly
- Always call
next()
to pass control - Call
next(err)
to skip to error handlers
- Always call
-
Modularize your middleware
- Export individual middleware functions
- Group related middleware in the same file
-
Handle middleware errors
- Use try/catch blocks in async middleware
- Or use express-async-errors package
Common Pitfalls
Section titled “Common Pitfalls”❌ 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
Conclusion
Section titled “Conclusion”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:
- Keep middleware functions focused on a single responsibility
- Use the correct order of middleware registration
- Handle errors appropriately
- Modularize your middleware code
- Use built-in and third-party middleware wisely