Express Core Knowledge

SkillMedia

Express.js minimal Node.js web framework. Covers routing, middleware, error handling, and API design. Use when building Express APIs.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Express Core Knowledge skill

What this skill tells your AI

The instructions your AI receives, as published by claude-dev-suite/claude-dev-suite in skills/backend-frameworks/express/SKILL.md and read by ahel’s review.

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: express for comprehensive documentation.

Basic Setup

import express from 'express';
import cors from 'cors';
import helmet from 'helmet';

const app = express();

// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());

// Routes
app.use('/api/users', userRoutes);

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

app.listen(3000);

Route Patterns

import { Router } from 'express';

const router = Router();

router.get('/', async (req, res, next) => {
  try {
    const users = await db.users.findMany();
    res.json(users);
  } catch (err) {
    next(err);
  }
});

router.post('/', async (req, res, next) => {
  try {
    const user = await db.users.create(req.body);
    res.status(201).json(user);
  } catch (err) {
    next(err);
  }
});

router.get('/:id', async (req, res, next) => {
  const user = await db.users.find(req.params.id);
  if (!user) return res.status(404).json({ error: 'Not found' });
  res.json(user);
});

Middleware Pattern

// Auth middleware
const authenticate = async (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'Unauthorized' });

  try {
    req.user = await verifyToken(token);
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
};

router.get('/protected', authenticate, handler);

Error Handler

const errorHandler = (err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || 'Internal Server Error'
  });
};

Production Readiness

Security Configuration

import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import slowDown from 'express-slow-down';
import hpp from 'hpp';

const app = express();

// Security headers
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
    },
  },
  hsts: { maxAge: 31536000, includeSubDomains: true },
}));

// CORS
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
}));

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100,                   // 100 requests per window
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests' },
});
app.use('/api', limiter);

// Slow down repeated requests
const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000,
  delayAfter: 50,
  delayMs: (hits) => hits * 100,
});
app.use('/api', speedLimiter);

// Prevent HTTP Parameter Pollution
app.use(hpp());

// Body parsing with limits
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));

// Trust proxy (for rate limiting behind reverse proxy)
app.set('trust proxy', 1);

Health Checks

// Health endpoint
app.get('/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

// Readiness endpoint (check dependencies)
app.get('/ready', async (req, res) => {
  try {
    await db.query('SELECT 1');
    res.json({ status: 'ready', database: 'connected' });
  } catch (error) {
    res.status(503).json({ status: 'not ready', database: 'disconnected' });
  }
});

// Liveness endpoint
app.get('/live', (req, res) => {
  res.status(200).send('OK');
});

Structured Logging

import pino from 'pino-http';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  redact: ['req.headers.authorization', 'req.body.password'],
  serializers: {
    req: (req) => ({
      method: req.method,
      url: req.url,
      query: req.query,
      params: req.params,
    }),
  },
});

app.use(logger);

Monitoring Metrics

MetricAlert Threshold
Request latency p99> 500ms
Error rate (5xx)> 1%
Memory usage> 80%
Event loop lag> 100ms
Active handles> 1000

Graceful Shutdown

const server = app.listen(PORT);

const gracefulShutdown = async (signal: string) => {
  console.log(`${signal} received, shutting down gracefully`);

  server.close(async () => {
    console.log('HTTP server closed');
    await db.disconnect();
    process.exit(0);
  });

  // Force shutdown after 30s
  setTimeout(() => {
    console.error('Forced shutdown');
    process.exit(1);
  }, 30000);
};

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

Error Handler (Production)

const errorHandler = (err, req, res, next) => {
  // Log error
  req.log.error({
    err,
    method: req.method,
    url: req.url,
    body: req.body,
  });

  // Don't leak error details in production
  const statusCode = err.statusCode || err.status || 500;
  const message = statusCode === 500 && process.env.NODE_ENV === 'production'
    ? 'Internal Server Error'
    : err.message;

  res.status(statusCode).json({
    error: message,
    ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
  });
};

// Must be last middleware
app.use(errorHandler);

Checklist

  • Helmet security headers enabled
  • CORS properly configured
  • Rate limiting on API endpoints
  • Body size limits configured
  • Input validation (express-validator/joi)
  • HPP protection enabled
  • Structured logging (no console.log)
  • Health/readiness/liveness endpoints
  • Graceful shutdown handling
  • Error details hidden in production
  • HTTPS/TLS in production
  • Trust proxy configured (if behind LB)

When NOT to Use This Skill

  • Enterprise Architecture: Use NestJS for dependency injection, decorators, and modular design
  • Maximum Performance: Use Fastify for schema-based validation and faster throughput
  • Edge Runtimes: Use Hono for Cloudflare Workers, Vercel Edge, or edge-first design
  • Type-Safe APIs: Consider tRPC with Express or use Fastify/NestJS for better TypeScript integration
  • WebSocket Implementation: Use dedicated WebSocket skill (coming soon)
  • GraphQL: Defer to graphql-expert for Apollo Server setup
  • Database Queries: Use prisma-expert or sql-expert for ORM/query specifics

Anti-Patterns

Anti-PatternWhy It's BadCorrect Approach
Using app.get('*') before specific routesCatches all requests, routes unreachablePlace catch-all routes last
Not using next() in middlewareRequest hangs, no response sentAlways call next() or send response
Synchronous error throwing without try-catchCrashes serverWrap in try-catch, use next(err)
Using res.send() multiple times"Headers already sent" errorSend response once per request
Not setting trust proxy behind load balancerWrong client IP, rate limiting failsSet app.set('trust proxy', 1)
Parsing body without size limitsDoS vulnerabilitySet limit: '10kb' in body parsers
Using app.use(express.static()) without pathServes entire filesystemSpecify explicit public directory
Mixing callback and promise stylesInconsistent error handlingUse async/await consistently

Quick Troubleshooting

IssueLikely CauseSolution
"Cannot set headers after sent"Multiple res.send() callsEnsure only one response per request
Middleware not executingRegistered after routesMove app.use() before route definitions
404 for all routesRoutes defined after app.listen()Define routes before calling listen()
Request hangs indefinitelyMiddleware missing next()Add next() or send response
CORS errors despite cors middlewareMiddleware order issuePlace app.use(cors()) before routes
Rate limiting not workingBehind proxy without trust proxySet app.set('trust proxy', 1)
Body parser returns undefinedWrong content-type headerEnsure Content-Type: application/json
Static files not servingWrong path configurationUse absolute path: express.static(path.join(__dirname, 'public'))

WebSocket Integration

Note: For WebSocket functionality, consider using a dedicated WebSocket library alongside Express.

Setup with ws

import express from 'express';
import { createServer } from 'http';
import { WebSocketServer, WebSocket } from 'ws';

const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });

// Connection handling
wss.on('connection', (ws: WebSocket, req) => {
  const userId = new URL(req.url!, `http://${req.headers.host}`).searchParams.get('userId');
  console.log(`Client connected: ${userId}`);

  ws.on('message', (data) => {
    const message = JSON.parse(data.toString());
    handleMessage(ws, message);
  });

  ws.on('close', () => {
    console.log(`Client disconnected: ${userId}`);
  });

  ws.on('error', (error) => {
    console.error('WebSocket error:', error);
  });
});

server.listen(3000);

Authentication

import { WebSocketServer } from 'ws';
import jwt from 'jsonwebtoken';

const wss = new WebSocketServer({
  server,
  verifyClient: async ({ req }, done) => {
    const token = req.url?.split('token=')[1];
    if (!token) return done(false, 401, 'Unauthorized');

    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET!);
      (req as any).user = decoded;
      done(true);
    } catch {
      done(false, 401, 'Invalid token');
    }
  },
});

Room Management

const rooms = new Map<string, Set<WebSocket>>();

function joinRoom(ws: WebSocket, roomId: string) {
  if (!rooms.has(roomId)) {
    rooms.set(roomId, new Set());
  }
  rooms.get(roomId)!.add(ws);
}

function leaveRoom(ws: WebSocket, roomId: string) {
  rooms.get(roomId)?.delete(ws);
  if (rooms.get(roomId)?.size === 0) {
    rooms.delete(roomId);
  }
}

function broadcastToRoom(roomId: string, message: object, exclude?: WebSocket) {
  const clients = rooms.get(roomId);
  if (!clients) return;

  const data = JSON.stringify(message);
  clients.forEach((client) => {
    if (client !== exclude && client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  });
}

Heartbeat & Connection Health

const HEARTBEAT_INTERVAL = 30000;

wss.on('connection', (ws: WebSocket) => {
  (ws as any).isAlive = true;

  ws.on('pong', () => {
    (ws as any).isAlive = true;
  });
});

const heartbeat = setInterval(() => {
  wss.clients.forEach((ws) => {
    if ((ws as any).isAlive === false) {
      return ws.terminate();
    }
    (ws as any).isAlive = false;
    ws.ping();
  });
}, HEARTBEAT_INTERVAL);

wss.on('close', () => clearInterval(heartbeat));

Message Protocol

interface WSMessage {
  type: string;
  payload: unknown;
  timestamp: number;
}

function handleMessage(ws: WebSocket, message: WSMessage) {
  switch (message.type) {
    case 'join_room':
      joinRoom(ws, message.payload as string);
      break;
    case 'leave_room':
      leaveRoom(ws, message.payload as string);
      break;
    case 'broadcast':
      broadcastToRoom(
        (message.payload as any).roomId,
        (message.payload as any).data
      );
      break;
    default:
      ws.send(JSON.stringify({ type: 'error', payload: 'Unknown message type' }));
  }
}

Scaling with Redis

import { createClient } from 'redis';

const pub = createClient({ url: process.env.REDIS_URL });
const sub = pub.duplicate();

await pub.connect();
await sub.connect();

// Subscribe to channel
await sub.subscribe('ws:broadcast', (message) => {
  const data = JSON.parse(message);
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify(data));
    }
  });
});

// Publish from any server instance
function publishMessage(channel: string, message: object) {
  pub.publish(channel, JSON.stringify(message));
}

Reference Documentation

Signals

GitHub stars
33
Forks
6
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
express
Source
github.com/claude-dev-suite/claude-dev-suite