# Express Development ## Overview **Express.js development specialist covering middleware organization, error handling, validation, database integration, testing, and production deployment.** **Core principle**: Express's minimalist philosophy requires disciplined patterns - without structure, Express apps become tangled middleware chains with inconsistent error handling and poor testability. ## When to Use This Skill Use when encountering: - **Middleware organization**: Ordering, async error handling, custom middleware - **Error handling**: Centralized handlers, custom error classes, async/await errors - **Request validation**: Zod, express-validator, type-safe validation - **Database patterns**: Connection pooling, transactions, graceful shutdown - **Testing**: Supertest, mocking, middleware isolation - **Production deployment**: PM2, clustering, Docker, environment management - **Performance**: Compression, caching, clustering - **Security**: Helmet, rate limiting, CORS, input sanitization **DO NOT use for**: - General TypeScript patterns (use `axiom-python-engineering` equivalents) - API design principles (use `rest-api-design`) - Database-agnostic patterns (use `database-integration`) ## Middleware Organization ### Correct Middleware Order **Order matters** - middleware executes top to bottom: ```typescript import express from 'express'; import helmet from 'helmet'; import cors from 'cors'; import compression from 'compression'; const app = express(); // 1. Security (FIRST - before any parsing) app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], }, }, })); // 2. CORS (before routes) app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(','), credentials: true, maxAge: 86400, // 24 hours })); // 3. Parsing app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // 4. Compression app.use(compression()); // 5. Logging app.use(morgan('combined', { stream: logger.stream })); // 6. Authentication (before routes that need it) app.use('/api', authenticationMiddleware); // 7. Routes app.use('/api/users', userRoutes); app.use('/api/posts', postRoutes); // 8. 404 handler (AFTER all routes) app.use((req, res) => { res.status(404).json({ status: 'error', message: 'Route not found', path: req.path, }); }); // 9. Error handler (LAST) app.use(errorHandler); ``` ### Async Error Wrapper **Problem**: Express doesn't catch async errors automatically ```typescript // src/middleware/asyncHandler.ts import { Request, Response, NextFunction } from 'express'; export const asyncHandler = ( fn: (req: Request, res: Response, next: NextFunction) => Promise ) => { return (req: Request, res: Response, next: NextFunction) => { Promise.resolve(fn(req, res, next)).catch(next); }; }; // Usage router.get('/:id', asyncHandler(async (req, res) => { const user = await userService.findById(req.params.id); if (!user) throw new NotFoundError('User not found'); res.json(user); })); ``` **Alternative**: Use express-async-errors (automatic) ```typescript // At top of app.ts (BEFORE routes) import 'express-async-errors'; // Now all async route handlers auto-catch errors router.get('/:id', async (req, res) => { const user = await userService.findById(req.params.id); res.json(user); }); // Errors automatically forwarded to error handler ``` ## Error Handling ### Custom Error Classes ```typescript // src/errors/AppError.ts export class AppError extends Error { constructor( public readonly message: string, public readonly statusCode: number, public readonly isOperational: boolean = true ) { super(message); Error.captureStackTrace(this, this.constructor); } } // src/errors/HttpErrors.ts export class BadRequestError extends AppError { constructor(message: string) { super(message, 400); } } export class UnauthorizedError extends AppError { constructor(message = 'Unauthorized') { super(message, 401); } } export class ForbiddenError extends AppError { constructor(message = 'Forbidden') { super(message, 403); } } export class NotFoundError extends AppError { constructor(message: string) { super(message, 404); } } export class ConflictError extends AppError { constructor(message: string) { super(message, 409); } } export class TooManyRequestsError extends AppError { constructor(message = 'Too many requests', public retryAfter?: number) { super(message, 429); } } ``` ### Centralized Error Handler ```typescript // src/middleware/errorHandler.ts import { Request, Response, NextFunction } from 'express'; import { AppError } from '../errors/AppError'; import { logger } from '../config/logger'; export const errorHandler = ( err: Error, req: Request, res: Response, next: NextFunction ) => { // Log error with context logger.error('Error occurred', { error: { message: err.message, stack: err.stack, name: err.name, }, request: { method: req.method, url: req.url, ip: req.ip, userAgent: req.get('user-agent'), }, }); // Operational errors (expected) if (err instanceof AppError && err.isOperational) { const response: any = { status: 'error', message: err.message, }; // Add retry-after for rate limiting if (err instanceof TooManyRequestsError && err.retryAfter) { res.setHeader('Retry-After', err.retryAfter); response.retryAfter = err.retryAfter; } return res.status(err.statusCode).json(response); } // Validation errors (Zod, express-validator) if (err.name === 'ZodError') { return res.status(400).json({ status: 'error', message: 'Validation failed', errors: (err as any).errors, }); } // Database constraint violations if ((err as any).code === '23505') { // PostgreSQL unique violation return res.status(409).json({ status: 'error', message: 'Resource already exists', }); } if ((err as any).code === '23503') { // Foreign key violation return res.status(400).json({ status: 'error', message: 'Invalid reference', }); } // Unexpected errors (don't leak details in production) res.status(500).json({ status: 'error', message: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message, ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }), }); }; ``` ### Global Error Handlers ```typescript // src/server.ts process.on('unhandledRejection', (reason: Error) => { logger.error('Unhandled Rejection', { reason }); // Graceful shutdown server.close(() => process.exit(1)); }); process.on('uncaughtException', (error: Error) => { logger.error('Uncaught Exception', { error }); process.exit(1); }); ``` ## Request Validation ### Zod Integration (Type-Safe) ```typescript // src/schemas/userSchema.ts import { z } from 'zod'; export const createUserSchema = z.object({ body: z.object({ email: z.string().email('Invalid email'), password: z.string() .min(8, 'Password must be at least 8 characters') .regex(/[A-Z]/, 'Password must contain uppercase') .regex(/[0-9]/, 'Password must contain number'), name: z.string().min(2).max(100), age: z.number().int().positive().max(150).optional(), }), }); export const getUserSchema = z.object({ params: z.object({ id: z.string().regex(/^\d+$/, 'ID must be numeric'), }), }); export const getUsersSchema = z.object({ query: z.object({ page: z.string().regex(/^\d+$/).transform(Number).default('1'), limit: z.string().regex(/^\d+$/).transform(Number).default('10'), search: z.string().optional(), sortBy: z.enum(['name', 'created_at', 'updated_at']).optional(), order: z.enum(['asc', 'desc']).optional(), }), }); // Type inference export type CreateUserInput = z.infer['body']; export type GetUserParams = z.infer['params']; export type GetUsersQuery = z.infer['query']; ``` **Validation middleware**: ```typescript // src/middleware/validate.ts import { Request, Response, NextFunction } from 'express'; import { AnyZodObject, ZodError } from 'zod'; export const validate = (schema: AnyZodObject) => { return async (req: Request, res: Response, next: NextFunction) => { try { const validated = await schema.parseAsync({ body: req.body, query: req.query, params: req.params, }); // Replace with validated data (transforms applied) req.body = validated.body || req.body; req.query = validated.query || req.query; req.params = validated.params || req.params; next(); } catch (error) { if (error instanceof ZodError) { return res.status(400).json({ status: 'error', message: 'Validation failed', errors: error.errors.map(err => ({ field: err.path.join('.'), message: err.message, code: err.code, })), }); } next(error); } }; }; ``` **Usage in routes**: ```typescript import { Router } from 'express'; import { validate } from '../middleware/validate'; import * as schemas from '../schemas/userSchema'; const router = Router(); router.post('/', validate(schemas.createUserSchema), async (req, res) => { // req.body is now typed as CreateUserInput const user = await userService.create(req.body); res.status(201).json(user); }); router.get('/:id', validate(schemas.getUserSchema), async (req, res) => { // req.params.id is validated const user = await userService.findById(req.params.id); if (!user) throw new NotFoundError('User not found'); res.json(user); }); ``` ## Database Connection Pooling ### PostgreSQL with pg ```typescript // src/config/database.ts import { Pool, PoolConfig } from 'pg'; import { logger } from './logger'; const config: PoolConfig = { host: process.env.DB_HOST || 'localhost', port: Number(process.env.DB_PORT) || 5432, database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, max: Number(process.env.DB_POOL_MAX) || 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, statement_timeout: 30000, // 30s query timeout }; export const pool = new Pool(config); // Event handlers pool.on('connect', (client) => { logger.debug('Database client connected'); }); pool.on('acquire', (client) => { logger.debug('Client acquired from pool'); }); pool.on('error', (err, client) => { logger.error('Unexpected pool error', { error: err }); process.exit(-1); }); // Health check export const testConnection = async () => { try { const client = await pool.connect(); const result = await client.query('SELECT NOW()'); client.release(); logger.info('Database connection successful', { serverTime: result.rows[0].now, }); } catch (err) { logger.error('Database connection failed', { error: err }); throw err; } }; // Graceful shutdown export const closePool = async () => { logger.info('Closing database pool'); await pool.end(); logger.info('Database pool closed'); }; ``` ### Transaction Helper ```typescript // src/utils/transaction.ts import { Pool, PoolClient } from 'pg'; export async function withTransaction( pool: Pool, callback: (client: PoolClient) => Promise ): Promise { const client = await pool.connect(); try { await client.query('BEGIN'); const result = await callback(client); await client.query('COMMIT'); return result; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } } // Usage import { pool } from '../config/database'; async function createUserWithProfile(userData, profileData) { return withTransaction(pool, async (client) => { const userResult = await client.query( 'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING id', [userData.email, userData.name] ); const userId = userResult.rows[0].id; await client.query( 'INSERT INTO profiles (user_id, bio) VALUES ($1, $2)', [userId, profileData.bio] ); return userId; }); } ``` ## Testing ### Integration Tests with Supertest ```typescript // tests/integration/userRoutes.test.ts import request from 'supertest'; import app from '../../src/app'; import { pool } from '../../src/config/database'; describe('User Routes', () => { beforeAll(async () => { await pool.query('CREATE TABLE IF NOT EXISTS users (...)'); }); afterEach(async () => { await pool.query('TRUNCATE TABLE users CASCADE'); }); afterAll(async () => { await pool.end(); }); describe('POST /api/users', () => { it('should create user with valid data', async () => { const response = await request(app) .post('/api/users') .send({ email: 'test@example.com', name: 'Test User', password: 'Password123', }) .expect(201); expect(response.body).toHaveProperty('id'); expect(response.body.email).toBe('test@example.com'); expect(response.body).not.toHaveProperty('password'); }); it('should return 400 for invalid email', async () => { const response = await request(app) .post('/api/users') .send({ email: 'invalid', name: 'Test', password: 'Password123', }) .expect(400); expect(response.body.status).toBe('error'); expect(response.body.errors).toContainEqual( expect.objectContaining({ field: 'body.email', message: expect.stringContaining('email'), }) ); }); }); describe('GET /api/users/:id', () => { it('should return user by ID', async () => { const createRes = await request(app) .post('/api/users') .send({ email: 'test@example.com', name: 'Test User', password: 'Password123', }); const response = await request(app) .get(`/api/users/${createRes.body.id}`) .expect(200); expect(response.body.id).toBe(createRes.body.id); }); it('should return 404 for non-existent user', async () => { await request(app) .get('/api/users/99999') .expect(404); }); }); }); ``` ### Unit Tests with Mocks ```typescript // tests/unit/userService.test.ts import { userService } from '../../src/services/userService'; import { pool } from '../../src/config/database'; jest.mock('../../src/config/database'); const mockPool = pool as jest.Mocked; describe('UserService', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('findById', () => { it('should return user when found', async () => { mockPool.query.mockResolvedValue({ rows: [{ id: 1, email: 'test@example.com', name: 'Test' }], command: 'SELECT', rowCount: 1, oid: 0, fields: [], }); const result = await userService.findById('1'); expect(result).toEqual( expect.objectContaining({ id: 1, email: 'test@example.com' }) ); }); it('should return null when not found', async () => { mockPool.query.mockResolvedValue({ rows: [], command: 'SELECT', rowCount: 0, oid: 0, fields: [], }); const result = await userService.findById('999'); expect(result).toBeNull(); }); }); }); ``` ## Production Deployment ### PM2 Configuration ```javascript // ecosystem.config.js module.exports = { apps: [{ name: 'api', script: './dist/server.js', instances: 'max', // Use all CPU cores exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000, }, error_file: './logs/err.log', out_file: './logs/out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss Z', merge_logs: true, max_memory_restart: '500M', wait_ready: true, listen_timeout: 10000, kill_timeout: 5000, }], }; ``` **Graceful shutdown with PM2**: ```typescript // src/server.ts const server = app.listen(PORT, () => { logger.info(`Server started on port ${PORT}`); // Signal PM2 ready if (process.send) { process.send('ready'); } }); // Graceful shutdown process.on('SIGINT', async () => { logger.info('SIGINT received, closing server'); server.close(async () => { await closePool(); logger.info('Server closed'); process.exit(0); }); // Force shutdown after 10s setTimeout(() => { logger.error('Forcing shutdown'); process.exit(1); }, 10000); }); ``` ### Dockerfile ```dockerfile # Multi-stage build FROM node:18-alpine AS builder WORKDIR /app # Copy package files COPY package*.json ./ COPY tsconfig.json ./ # Install dependencies RUN npm ci # Copy source COPY src ./src # Build TypeScript RUN npm run build # Production image FROM node:18-alpine WORKDIR /app # Install production dependencies only COPY package*.json ./ RUN npm ci --omit=dev && npm cache clean --force # Copy built files COPY --from=builder /app/dist ./dist # Create non-root user RUN addgroup -g 1001 -S nodejs && \ adduser -S nodejs -u 1001 USER nodejs EXPOSE 3000 CMD ["node", "dist/server.js"] ``` ### Health Check Endpoint ```typescript // src/routes/healthRoutes.ts import { Router } from 'express'; import { pool } from '../config/database'; const router = Router(); router.get('/health', async (req, res) => { const health = { uptime: process.uptime(), message: 'OK', timestamp: Date.now(), }; try { await pool.query('SELECT 1'); health.database = 'connected'; } catch (error) { health.database = 'disconnected'; return res.status(503).json(health); } res.json(health); }); router.get('/health/ready', async (req, res) => { // Readiness check try { await pool.query('SELECT 1'); res.status(200).json({ status: 'ready' }); } catch (error) { res.status(503).json({ status: 'not ready' }); } }); router.get('/health/live', (req, res) => { // Liveness check (simpler) res.status(200).json({ status: 'alive' }); }); export default router; ``` ## Performance Optimization ### Response Caching ```typescript import Redis from 'ioredis'; const redis = new Redis({ host: process.env.REDIS_HOST, port: Number(process.env.REDIS_PORT), }); export const cacheMiddleware = (duration: number) => { return async (req: Request, res: Response, next: NextFunction) => { if (req.method !== 'GET') return next(); const key = `cache:${req.originalUrl}`; try { const cached = await redis.get(key); if (cached) { return res.json(JSON.parse(cached)); } // Capture response const originalJson = res.json.bind(res); res.json = (body: any) => { redis.setex(key, duration, JSON.stringify(body)); return originalJson(body); }; next(); } catch (error) { next(); } }; }; // Usage router.get('/users', cacheMiddleware(300), async (req, res) => { const users = await userService.findAll(); res.json(users); }); ``` ## Security ### Rate Limiting ```typescript import rateLimit from 'express-rate-limit'; import RedisStore from 'rate-limit-redis'; import Redis from 'ioredis'; const redis = new Redis(); export const apiLimiter = rateLimit({ store: new RedisStore({ client: redis }), windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per window message: 'Too many requests, please try again later', standardHeaders: true, legacyHeaders: false, }); export const authLimiter = rateLimit({ store: new RedisStore({ client: redis }), windowMs: 15 * 60 * 1000, max: 5, // 5 attempts skipSuccessfulRequests: true, }); // Usage app.use('/api/', apiLimiter); app.use('/api/auth/login', authLimiter); ``` ## Anti-Patterns | Anti-Pattern | Why Bad | Fix | |--------------|---------|-----| | **No async error handling** | Crashes server | Use asyncHandler or express-async-errors | | **Inconsistent error responses** | Poor DX | Centralized error handler | | **New DB connection per request** | Exhausts connections | Use connection pool | | **No graceful shutdown** | Data loss, broken requests | Handle SIGTERM/SIGINT | | **Logging to console in production** | Lost logs, no structure | Use Winston/Pino with transports | | **No request validation** | Security vulnerabilities | Zod/express-validator | | **Synchronous operations in routes** | Blocks event loop | Use async/await | | **No health checks** | Can't monitor service | /health endpoints | ## Cross-References **Related skills**: - **Database patterns** → `database-integration` (pooling, transactions) - **API testing** → `api-testing` (supertest patterns) - **REST design** → `rest-api-design` (endpoint patterns) - **Authentication** → `api-authentication` (JWT, sessions) ## Further Reading - **Express docs**: https://expressjs.com/ - **Express.js Best Practices**: https://expressjs.com/en/advanced/best-practice-performance.html - **Node.js Production Best Practices**: https://github.com/goldbergyoni/nodebestpractices