Skip to content

Express Middleware

Middleware functions are the backbone of Express. They have access to the request, response, and next function, and can modify the request/response or end the request cycle.

import { Request, Response, NextFunction } from 'express';
function myMiddleware(req: Request, res: Response, next: NextFunction) {
// Do something before the route handler
console.log(`${req.method} ${req.path}`);
// Call next() to pass control to the next middleware or route
next();
// If you call next(error), Express skips to error-handling middleware
// next(new Error('Something went wrong'));
}
app.use(myMiddleware); // applies to all routes
app.use('/api', myMiddleware); // applies to /api/* routes only
app.get('/path', myMiddleware, handler); // applies to this route only

Middleware runs in registration order:

app.use(cors()); // 1. CORS headers
app.use(helmet()); // 2. Security headers
app.use(express.json()); // 3. Parse request body
app.use(requestLogger); // 4. Log incoming request
app.use('/api', router); // 5. Routes
app.use(errorHandler); // 6. Error handler (must be last)
function requestLogger(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`);
});
next();
}

Or use morgan:

Terminal window
npm install morgan
npm install -D @types/morgan
import morgan from 'morgan';
app.use(morgan('dev')); // concise output for development
app.use(morgan('combined')); // Apache-style for production
Terminal window
npm install cors
npm install -D @types/cors
import cors from 'cors';
// Allow all origins (development)
app.use(cors());
// Restrict to specific origins
app.use(cors({
origin: ['https://myapp.com', 'https://staging.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
Terminal window
npm install helmet
import helmet from 'helmet';
app.use(helmet()); // sets 15+ security-related HTTP headers
Terminal window
npm install express-rate-limit
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per window
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later' },
});
app.use('/api', limiter);
// Stricter limit for auth endpoints
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10 });
app.use('/api/auth', authLimiter);
import jwt from 'jsonwebtoken';
export function requireAuth(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authentication required' });
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
req.user = { id: payload.sub as string };
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Attach to protected routes
router.get('/profile', requireAuth, getProfile);
router.post('/posts', requireAuth, createPost);

Error middleware must have exactly four parameters. Express identifies it by the err parameter:

function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
console.error(err.stack);
if (err.name === 'ValidationError') {
return res.status(400).json({ error: err.message });
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ error: 'Unauthorized' });
}
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message,
});
}
// Must be registered AFTER all other middleware and routes
app.use(errorHandler);

Express doesn’t catch async errors automatically. Wrap async route handlers:

// Manual approach
app.get('/users', async (req, res, next) => {
try {
const users = await userService.findAll();
res.json(users);
} catch (err) {
next(err); // passes error to error handler
}
});
// Helper to avoid boilerplate
const asyncHandler = (fn: RequestHandler): RequestHandler =>
(req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users', asyncHandler(async (req, res) => {
const users = await userService.findAll();
res.json(users);
}));
src/types/express.d.ts
declare global {
namespace Express {
interface Request {
user?: { id: string; role: string };
userId?: string;
}
}
}