Express.js Reference: Routing, Middleware, JWT Auth, Zod Validation & Production Setup
Express.js is Node.js’s most widely used HTTP framework. It’s minimal by design — routing, middleware, and response helpers, nothing else. Understanding the middleware chain, Router, and error handling unlocks everything else in the ecosystem.
1. App Setup, Routing & Middleware
Create an app, define routes, and understand the middleware execution order
import express, { Request, Response, NextFunction } from 'express';
const app = express();
// Built-in middleware:
app.use(express.json()); // parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // parse form data
app.use(express.static('public')); // serve static files
// Custom middleware (runs for every request):
app.use((req: Request, res: Response, next: NextFunction) => {
console.log(`${req.method} ${req.path} - ${Date.now()}`);
next(); // MUST call next() or the request hangs
});
// Routes:
app.get('/users', async (req, res) => {
const { page = '1', limit = '20', status } = req.query;
const users = await User.find({ status }).limit(Number(limit));
res.json({ users, page: Number(page) });
});
app.get('/users/:id', async (req, res) => {
const { id } = req.params;
const user = await User.findById(id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
app.post('/users', async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
// Middleware execution order matters:
// 1. app.use() middleware runs in registration order
// 2. Route-specific middleware runs before the route handler
// 3. Error middleware (4 params) only runs when next(err) is called
app.listen(3000, () => console.log('Server on :3000'));
2. Router — Modular Route Organisation
Express Router for splitting routes into separate files
// routes/users.ts — isolated router:
import { Router } from 'express';
import { authenticate } from '../middleware/auth';
const router = Router();
// Route-level middleware (runs only for routes in this router):
router.use(authenticate); // all /users/* routes require auth
router.get('/', async (req, res) => {
res.json(await User.findAll());
});
router.get('/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
router.post('/', async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
router.patch('/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true });
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
router.delete('/:id', async (req, res) => {
await User.findByIdAndDelete(req.params.id);
res.status(204).send();
});
export default router;
// app.ts — mount routers:
import userRouter from './routes/users';
import orderRouter from './routes/orders';
app.use('/api/users', userRouter); // all routes prefixed with /api/users
app.use('/api/orders', orderRouter);
3. Authentication Middleware
JWT middleware, role-based access control, and API key auth
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
// Extend Request type to include user:
declare global {
namespace Express {
interface Request {
user?: { id: string; role: string };
}
}
}
// JWT middleware:
export function authenticate(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; role: string };
req.user = { id: payload.sub, role: payload.role };
next();
} catch {
return res.status(401).json({ error: 'Invalid token' });
}
}
// Role-based access control (returns middleware):
export function requireRole(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// Usage:
router.delete('/:id', authenticate, requireRole('admin'), async (req, res) => {
await User.findByIdAndDelete(req.params.id);
res.status(204).send();
});
// Login route (issue JWT):
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !await user.comparePassword(password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET!, { expiresIn: '7d' });
res.json({ token });
});
4. Error Handling & Validation
Centralised async error handling, Zod validation middleware, and 404 catch-all
// Async error wrapper (avoid try/catch in every route):
export const asyncHandler = (fn: Function) =>
(req: Request, res: Response, next: NextFunction) =>
Promise.resolve(fn(req, res, next)).catch(next);
// Use it:
router.get('/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id); // if this throws, next(err) is called
if (!user) throw Object.assign(new Error('Not found'), { status: 404 });
res.json(user);
}));
// Global error handler (4 params — MUST be registered last):
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
const status = err.status || err.statusCode || 500;
const message = status < 500 ? err.message : 'Internal server error';
// Log 500s:
if (status >= 500) console.error(err);
res.status(status).json({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
});
// 404 catch-all (register after all routes, before error handler):
app.use((req, res) => res.status(404).json({ error: `${req.path} not found` }));
// Zod validation middleware:
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150),
});
export function validate(schema: z.ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({ errors: result.error.flatten().fieldErrors });
}
req.body = result.data; // replace with parsed + typed data
next();
};
}
// Usage:
router.post('/', validate(CreateUserSchema), asyncHandler(async (req, res) => {
const user = await User.create(req.body); // req.body is typed + validated
res.status(201).json(user);
}));
5. Rate Limiting, CORS & Production Setup
express-rate-limit, CORS config, helmet, and cluster mode
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import cluster from 'cluster';
import os from 'os';
// Security headers (always use in production):
app.use(helmet()); // sets X-Content-Type-Options, CSP, etc.
// CORS:
app.use(cors({
origin: process.env.NODE_ENV === 'production'
? ['https://app.example.com']
: true, // allow all in dev
credentials: true, // allow cookies/auth headers
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
}));
// Rate limiting:
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window per IP
standardHeaders: true, // return RateLimit-* headers
legacyHeaders: false,
message: { error: 'Too many requests' },
});
app.use('/api', limiter); // apply to all /api/* routes
// Stricter limit for auth endpoints:
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10 });
app.use('/auth', authLimiter);
// Cluster mode (use all CPU cores):
if (cluster.isPrimary) {
const numCPUs = os.cpus().length;
for (let i = 0; i < numCPUs; i++) cluster.fork();
cluster.on('exit', () => cluster.fork()); // restart crashed workers
} else {
app.listen(3000);
}
// Health check endpoint (for K8s probes):
app.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime() }));
// npm deps: express helmet cors express-rate-limit
// dev: @types/express typescript ts-node-dev zod
Track Express.js and Node.js releases.
ReleaseRun monitors Node.js, React, and 13+ technologies.
Related: Node.js Reference | TypeScript Reference | OpenAPI Reference | Nodejs EOL Tracker
🔍 Free tool: npm Package Health Checker — check Express.js and middleware packages — body-parser, cors, helmet — for known CVEs and active maintenance.
Founded
2023 in London, UK
Contact
hello@releaserun.com