--- name: express-api-patterns description: Express.js API development, route handling, middleware, error handling, request validation, CORS. Use when building Express routes, implementing middleware, handling API requests, or setting up the backend server. allowed-tools: Read, Grep, Glob --- # Express API Patterns ## Core Principles 1. **RESTful Design** - Use HTTP methods appropriately (GET, POST, PUT, DELETE) 2. **Middleware First** - Use middleware for cross-cutting concerns 3. **Error Handling** - Centralized error handling middleware 4. **Validation** - Validate all inputs before processing 5. **Security** - CORS, rate limiting, input sanitization --- ## Server Setup Pattern ### CORRECT: Well-Structured Express Server ```javascript // server/index.js import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); // Import routes import authRoutes from './routes/auth.js'; import generateRoutes from './routes/generate.js'; import imageRoutes from './routes/images.js'; const app = express(); const PORT = process.env.PORT || 3001; // ===== Middleware ===== // CORS configuration app.use(cors({ origin: process.env.CLIENT_URL || 'http://localhost:5173', credentials: true })); // Body parsing app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Request logging (development only) if (process.env.NODE_ENV === 'development') { app.use((req, res, next) => { console.log(`${req.method} ${req.path}`); next(); }); } // ===== Routes ===== app.use('/api/auth', authRoutes); app.use('/api/generate', generateRoutes); app.use('/api/images', imageRoutes); // Health check app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // ===== Error Handling ===== // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Endpoint not found' }); }); // Global error handler app.use((err, req, res, next) => { console.error('Error:', err); const status = err.status || 500; const message = err.message || 'Internal server error'; res.status(status).json({ error: message }); }); // ===== Start Server ===== app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); }); export default app; ``` --- ## Route Pattern ### CORRECT: Well-Structured Route ```javascript // server/routes/generate.js import express from 'express'; import { generatePage, generatePageStream } from '../services/claude.js'; import fs from 'fs/promises'; import path from 'path'; const router = express.Router(); // Load system prompt let systemPrompt = ''; try { systemPrompt = await fs.readFile( path.join(process.cwd(), 'prompts', 'system.txt'), 'utf-8' ); } catch (error) { console.error('Failed to load system prompt:', error); } /** * POST /api/generate * Generate or update instructional page */ router.post('/', async (req, res, next) => { try { // 1. Extract and validate input const { config, message, history = [] } = req.body; if (!config || !config.topic) { return res.status(400).json({ error: 'Topic is required' }); } if (!message) { return res.status(400).json({ error: 'Message is required' }); } if (config.depthLevel < 0 || config.depthLevel > 4) { return res.status(400).json({ error: 'Depth level must be 0-4' }); } // 2. Call service const result = await generatePage(systemPrompt, config, message, history); // 3. Return response res.json({ message: result.message, html: result.html, timestamp: new Date().toISOString() }); } catch (error) { // Pass to error handler next(error); } }); /** * POST /api/generate/stream * Generate page with streaming response */ router.post('/stream', async (req, res, next) => { try { const { config, message, history = [] } = req.body; // Validation (same as above) if (!config?.topic || !message) { return res.status(400).json({ error: 'Invalid request' }); } // Set headers for Server-Sent Events res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Stream generation await generatePageStream(systemPrompt, config, message, history, (chunk) => { res.write(`data: ${JSON.stringify(chunk)}\n\n`); }); res.end(); } catch (error) { next(error); } }); export default router; ``` ### WRONG: Poor Route Structure ```javascript // ❌ DON'T DO THIS router.post('/generate', (req, res) => { // ❌ No input validation // ❌ No error handling // ❌ Directly accessing nested properties without checks generatePage(req.body.config.topic, req.body.message).then(result => { res.send(result); // ❌ Not using res.json() }); }); ``` --- ## Middleware Patterns ### Authentication Middleware ```javascript // server/middleware/auth.js export const verifyPassword = (req, res, next) => { const { password } = req.body; const correctPassword = process.env.FACULTY_PASSWORD; if (!correctPassword) { return res.status(500).json({ error: 'Server configuration error' }); } if (password !== correctPassword) { return res.status(401).json({ error: 'Invalid password' }); } next(); // Password correct, proceed }; // Usage in route import { verifyPassword } from '../middleware/auth.js'; router.post('/verify', verifyPassword, (req, res) => { res.json({ success: true }); }); ``` ### Request Validation Middleware ```javascript // server/middleware/validate.js export const validateGenerateRequest = (req, res, next) => { const { config, message } = req.body; const errors = []; if (!config) { errors.push('config is required'); } else { if (!config.topic) errors.push('config.topic is required'); if (config.depthLevel === undefined) errors.push('config.depthLevel is required'); if (config.depthLevel < 0 || config.depthLevel > 4) { errors.push('config.depthLevel must be 0-4'); } } if (!message) { errors.push('message is required'); } if (errors.length > 0) { return res.status(400).json({ error: errors.join(', ') }); } next(); }; // Usage router.post('/', validateGenerateRequest, async (req, res, next) => { // Request is validated // ... handle request }); ``` ### Rate Limiting Middleware ```javascript // server/middleware/rateLimit.js import rateLimit from 'express-rate-limit'; export const generateLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 50, // 50 requests per window message: { error: 'Too many requests, please try again later' }, standardHeaders: true, legacyHeaders: false }); // Usage router.post('/', generateLimiter, async (req, res, next) => { // Rate limited endpoint }); ``` --- ## Error Handling Patterns ### Custom Error Classes ```javascript // server/utils/errors.js export class ValidationError extends Error { constructor(message) { super(message); this.name = 'ValidationError'; this.status = 400; } } export class APIError extends Error { constructor(message, status = 500) { super(message); this.name = 'APIError'; this.status = status; } } // Usage in route import { ValidationError, APIError } from '../utils/errors.js'; router.post('/', async (req, res, next) => { try { if (!req.body.config) { throw new ValidationError('Config is required'); } const result = await someAPICall(); if (!result) { throw new APIError('API call failed', 503); } res.json(result); } catch (error) { next(error); // Pass to error handler } }); ``` ### Centralized Error Handler ```javascript // server/middleware/errorHandler.js export const errorHandler = (err, req, res, next) => { // Log error console.error('Error:', { name: err.name, message: err.message, stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, url: req.url, method: req.method }); // Determine status and message const status = err.status || 500; const message = err.message || 'Internal server error'; // Send response res.status(status).json({ error: message, ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) }); }; // In server setup app.use(errorHandler); ``` --- ## Service Layer Pattern Separate business logic from route handlers: ```javascript // server/services/claude.js import Anthropic from '@anthropic-ai/sdk'; const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); export const generatePage = async (systemPrompt, config, message, history) => { // Business logic here const messages = [ ...history.map(msg => ({ role: msg.role, content: msg.content })), { role: 'user', content: buildPrompt(config, message) } ]; try { const response = await anthropic.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 8192, system: systemPrompt, messages: messages }); return { message: extractMessage(response.content[0].text), html: extractHTML(response.content[0].text) }; } catch (error) { throw new APIError(`Claude API error: ${error.message}`, 503); } }; // Helper functions const buildPrompt = (config, message) => { let prompt = message + '\n\n'; prompt += `Topic: ${config.topic}\n`; prompt += `Depth Level: ${config.depthLevel}\n`; if (config.styleFlags?.length > 0) { prompt += `Style Flags: ${config.styleFlags.join(', ')}\n`; } return prompt; }; const extractHTML = (text) => { const match = text.match(/```html\n([\s\S]*?)\n```/); if (!match) throw new Error('Could not extract HTML'); return match[1].trim(); }; const extractMessage = (text) => { return text.split('```html')[0].trim(); }; ``` --- ## File Upload Pattern ```javascript // server/routes/images.js import express from 'express'; import multer from 'multer'; import { uploadToCloudinary } from '../services/cloudinary.js'; const router = express.Router(); // Configure multer for memory storage const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 // 10MB }, fileFilter: (req, file, cb) => { // Only allow images if (!file.mimetype.startsWith('image/')) { return cb(new Error('Only image files allowed')); } cb(null, true); } }); /** * POST /api/images/upload * Upload image to Cloudinary */ router.post('/upload', upload.single('image'), async (req, res, next) => { try { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } // Upload to Cloudinary const result = await uploadToCloudinary(req.file.buffer); res.json({ url: result.secure_url, publicId: result.public_id }); } catch (error) { next(error); } }); // Multer error handling router.use((error, req, res, next) => { if (error instanceof multer.MulterError) { if (error.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'File too large (max 10MB)' }); } return res.status(400).json({ error: error.message }); } next(error); }); export default router; ``` --- ## Environment Configuration ```javascript // server/config/index.js import dotenv from 'dotenv'; dotenv.config(); const config = { port: parseInt(process.env.PORT || '3001', 10), nodeEnv: process.env.NODE_ENV || 'development', faculty: { password: process.env.FACULTY_PASSWORD }, anthropic: { apiKey: process.env.ANTHROPIC_API_KEY }, openai: { apiKey: process.env.OPENAI_API_KEY }, cloudinary: { cloudName: process.env.CLOUDINARY_CLOUD_NAME, apiKey: process.env.CLOUDINARY_API_KEY, apiSecret: process.env.CLOUDINARY_API_SECRET }, cors: { origin: process.env.CLIENT_URL || 'http://localhost:5173' } }; // Validate required config const validateConfig = () => { const required = [ 'faculty.password', 'anthropic.apiKey', 'openai.apiKey' ]; const missing = required.filter(path => { const value = path.split('.').reduce((obj, key) => obj?.[key], config); return !value; }); if (missing.length > 0) { throw new Error(`Missing required config: ${missing.join(', ')}`); } }; validateConfig(); export default config; ``` --- ## Testing Express Routes ```javascript // server/routes/generate.test.js import { describe, it, expect, vi, beforeEach } from 'vitest'; import request from 'supertest'; import express from 'express'; import generateRoutes from './generate.js'; // Mock the claude service vi.mock('../services/claude.js', () => ({ generatePage: vi.fn() })); import { generatePage } from '../services/claude.js'; const app = express(); app.use(express.json()); app.use('/api/generate', generateRoutes); describe('Generate Routes', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should generate page successfully', async () => { generatePage.mockResolvedValue({ message: 'Generated successfully', html: '...' }); const response = await request(app) .post('/api/generate') .send({ config: { topic: 'React', depthLevel: 2 }, message: 'Create a page', history: [] }); expect(response.status).toBe(200); expect(response.body.html).toBe('...'); }); it('should validate required fields', async () => { const response = await request(app) .post('/api/generate') .send({ config: { depthLevel: 2 }, // Missing topic message: 'Test' }); expect(response.status).toBe(400); expect(response.body.error).toContain('topic'); }); it('should handle errors gracefully', async () => { generatePage.mockRejectedValue(new Error('API error')); const response = await request(app) .post('/api/generate') .send({ config: { topic: 'Test', depthLevel: 2 }, message: 'Test' }); expect(response.status).toBe(500); }); }); ``` --- ## Checklist ### Before Creating Route - [ ] What HTTP method is appropriate? - [ ] What validation is needed? - [ ] What middleware should be applied? - [ ] What error cases need handling? - [ ] Should logic be in service layer? ### After Creating Route - [ ] Input validation implemented - [ ] Error handling in place - [ ] Success response well-structured - [ ] Status codes appropriate - [ ] Service layer used for business logic - [ ] Tests written - [ ] Documentation added --- ## Integration with Other Skills - **api-client-patterns**: Frontend consumption of these APIs - **prompt-engineering**: Claude API integration - **react-component-patterns**: Using API responses in UI - **systematic-debugging**: Debugging API issues --- ## Common Mistakes to Avoid 1. ❌ No input validation 2. ❌ Not using try/catch with async 3. ❌ Business logic in route handlers 4. ❌ Inconsistent error responses 5. ❌ Missing CORS configuration 6. ❌ Hard-coded configuration values 7. ❌ No request logging 8. ❌ Missing rate limiting 9. ❌ Not using middleware for common tasks 10. ❌ Ignoring security best practices