Skip to content

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