javascript-today

Express Middleware

Middleware functions are the backbone of Express applications. They have access to the request and response objects and can modify them, end the request, or pass control to the next middleware.

What is Middleware?

Middleware is a function that executes between receiving a request and sending a response:

Request → Middleware 1 → Middleware 2 → Route Handler → Response

Basic middleware structure:

function myMiddleware(req, res, next) {
  // Do something with req or res
  console.log('Middleware executed');
  
  // Pass control to next middleware
  next();
}

Three parameters:

  • req - Request object
  • res - Response object
  • next - Function to call next middleware

Built-in Middleware

Express includes several built-in middleware functions:

JSON Body Parser

const express = require('express');
const app = express();

// Parse JSON request bodies
app.use(express.json());

app.post('/api/users', (req, res) => {
  console.log(req.body); // { name: 'Alice', email: 'alice@example.com' }
  res.json({ message: 'User created', user: req.body });
});

URL-Encoded Parser

// Parse URL-encoded form data
app.use(express.urlencoded({ extended: true }));

app.post('/form', (req, res) => {
  console.log(req.body); // { username: 'alice', password: '...' }
  res.send('Form received');
});

Static Files

// Serve static files from 'public' directory
app.use(express.static('public'));

// Now files in public/ are accessible:
// public/style.css → http://localhost:3000/style.css
// public/app.js → http://localhost:3000/app.js

Application-Level Middleware

Runs for every request:

const express = require('express');
const app = express();

// Logger middleware
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
  next();
});

// Add timestamp to all requests
app.use((req, res, next) => {
  req.timestamp = Date.now();
  next();
});

// Use timestamp in routes
app.get('/', (req, res) => {
  res.json({ 
    message: 'Hello',
    requestTime: req.timestamp 
  });
});

Order matters - middleware executes in the order it’s defined:

// This runs first
app.use((req, res, next) => {
  console.log('First');
  next();
});

// This runs second
app.use((req, res, next) => {
  console.log('Second');
  next();
});

// This runs third
app.get('/', (req, res) => {
  console.log('Route handler');
  res.send('Done');
});

// Logs: First, Second, Route handler

Router-Level Middleware

Apply middleware only to specific routers:

const express = require('express');
const router = express.Router();

// Middleware for all routes in this router
router.use((req, res, next) => {
  console.log('Router middleware');
  next();
});

router.get('/users', (req, res) => {
  res.json({ users: [] });
});

router.get('/posts', (req, res) => {
  res.json({ posts: [] });
});

const app = express();
app.use('/api', router);

// Middleware only runs for /api/users and /api/posts

Route-Level Middleware

Apply to specific routes:

// Single middleware
function authenticate(req, res, next) {
  const token = req.headers.authorization;
  
  if (token === 'valid-token') {
    next(); // Continue to route
  } else {
    res.status(401).json({ error: 'Unauthorized' });
  }
}

app.get('/public', (req, res) => {
  res.json({ message: 'Public route' });
});

app.get('/private', authenticate, (req, res) => {
  res.json({ message: 'Private data' });
});

// Multiple middleware functions
function middleware1(req, res, next) {
  console.log('Middleware 1');
  next();
}

function middleware2(req, res, next) {
  console.log('Middleware 2');
  next();
}

app.get('/multi', [middleware1, middleware2], (req, res) => {
  res.send('Response');
});
// Or:
app.get('/multi', middleware1, middleware2, (req, res) => {
  res.send('Response');
});

Custom Middleware Examples

Request Logger

function requestLogger(req, res, next) {
  const start = Date.now();
  
  // Log when response finishes
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} - ${res.statusCode} - ${duration}ms`);
  });
  
  next();
}

app.use(requestLogger);

Authentication Middleware

function requireAuth(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  // Simplified - in real app, verify JWT token
  if (token === 'valid-token') {
    req.user = { id: 1, username: 'alice' };
    next();
  } else {
    res.status(403).json({ error: 'Invalid token' });
  }
}

app.get('/api/profile', requireAuth, (req, res) => {
  res.json({ user: req.user });
});

Validation Middleware

function validateUser(req, res, next) {
  const { name, email } = req.body;
  
  const errors = [];
  
  if (!name || name.length < 2) {
    errors.push('Name must be at least 2 characters');
  }
  
  if (!email || !email.includes('@')) {
    errors.push('Valid email is required');
  }
  
  if (errors.length > 0) {
    return res.status(400).json({ errors });
  }
  
  next();
}

app.post('/api/users', validateUser, (req, res) => {
  res.json({ message: 'User created', user: req.body });
});

Rate Limiting (Simple)

const requests = new Map();

function rateLimit(maxRequests = 100, windowMs = 60000) {
  return (req, res, next) => {
    const ip = req.ip;
    const now = Date.now();
    
    if (!requests.has(ip)) {
      requests.set(ip, []);
    }
    
    const userRequests = requests.get(ip);
    
    // Remove old requests outside time window
    const validRequests = userRequests.filter(time => now - time < windowMs);
    
    if (validRequests.length >= maxRequests) {
      return res.status(429).json({ 
        error: 'Too many requests' 
      });
    }
    
    validRequests.push(now);
    requests.set(ip, validRequests);
    
    next();
  };
}

app.use('/api', rateLimit(10, 60000)); // 10 requests per minute

CORS Middleware

function cors(req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  
  // Handle preflight requests
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  
  next();
}

app.use(cors);

Error Handling Middleware

Special middleware with 4 parameters to handle errors:

// Error handling middleware (must have 4 params)
function errorHandler(err, req, res, next) {
  console.error('Error:', err.message);
  
  res.status(err.status || 500).json({
    error: {
      message: err.message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    }
  });
}

// Regular routes
app.get('/api/error', (req, res) => {
  throw new Error('Something went wrong!');
});

app.get('/api/users/:id', (req, res, next) => {
  const id = parseInt(req.params.id);
  
  if (isNaN(id)) {
    const error = new Error('Invalid user ID');
    error.status = 400;
    return next(error); // Pass error to error handler
  }
  
  res.json({ id, name: 'User' });
});

// Error handler must be last
app.use(errorHandler);

Async Error Handling

Catch errors in async route handlers:

// Wrapper for async route handlers
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// Use with async routes
app.get('/api/data', asyncHandler(async (req, res) => {
  const data = await someAsyncOperation();
  res.json(data);
}));

// Or with Express 5+ (built-in async support)
app.get('/api/data', async (req, res) => {
  const data = await someAsyncOperation();
  res.json(data);
});

Third-Party Middleware

Popular middleware packages:

npm install morgan helmet compression cookie-parser
const express = require('express');
const morgan = require('morgan');      // Logging
const helmet = require('helmet');      // Security headers
const compression = require('compression'); // Gzip compression
const cookieParser = require('cookie-parser'); // Parse cookies

const app = express();

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

// Gzip compression
app.use(compression());

// HTTP request logger
app.use(morgan('combined'));

// Parse cookies
app.use(cookieParser());

// Access cookies
app.get('/', (req, res) => {
  console.log('Cookies:', req.cookies);
  res.cookie('user', 'alice', { maxAge: 900000 });
  res.send('Cookie set');
});

Complete Example

const express = require('express');
const app = express();

// Built-in middleware
app.use(express.json());
app.use(express.static('public'));

// Custom logger
app.use((req, res, next) => {
  console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
  next();
});

// Add request ID
app.use((req, res, next) => {
  req.id = Math.random().toString(36).substr(2, 9);
  next();
});

// Authentication middleware
function requireAuth(req, res, next) {
  const token = req.headers.authorization;
  
  if (token === 'secret') {
    req.user = { id: 1, username: 'alice' };
    next();
  } else {
    res.status(401).json({ error: 'Unauthorized' });
  }
}

// Validation middleware
function validateProduct(req, res, next) {
  const { name, price } = req.body;
  
  if (!name || !price || price < 0) {
    return res.status(400).json({ 
      error: 'Invalid product data' 
    });
  }
  
  next();
}

// Public route
app.get('/api/products', (req, res) => {
  res.json([
    { id: 1, name: 'Laptop', price: 999 }
  ]);
});

// Protected route with validation
app.post('/api/products', requireAuth, validateProduct, (req, res) => {
  res.status(201).json({
    message: 'Product created',
    product: req.body,
    createdBy: req.user.username,
    requestId: req.id
  });
});

// Error handler
app.use((err, req, res, next) => {
  console.error('Error:', err.message);
  res.status(500).json({ error: 'Internal server error' });
});

// 404 handler (must be last)
app.use((req, res) => {
  res.status(404).json({ error: 'Not found' });
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Best Practices

DO:

  • Call next() to pass control
  • Put error handlers last
  • Use built-in middleware when possible
  • Keep middleware focused (single responsibility)
  • Use third-party middleware for common tasks

DON’T:

  • Forget to call next()
  • Put routes before middleware they need
  • Modify req or res without documenting it
  • Use blocking operations in middleware
  • Ignore error handling

Summary

Type Usage Example
Application app.use(fn) Runs for all routes
Router router.use(fn) Runs for router routes
Route app.get('/path', fn, handler) Runs for specific route
Error app.use((err, req, res, next) => {}) Handles errors
Built-in express.json() Parse JSON bodies
Third-party morgan() HTTP logging