Claude-skill-registry express-api-patterns
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.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/express-api-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-express-api-patterns && rm -rf "$T"
manifest:
skills/data/express-api-patterns/SKILL.mdsource content
Express API Patterns
Core Principles
- RESTful Design - Use HTTP methods appropriately (GET, POST, PUT, DELETE)
- Middleware First - Use middleware for cross-cutting concerns
- Error Handling - Centralized error handling middleware
- Validation - Validate all inputs before processing
- Security - CORS, rate limiting, input sanitization
Server Setup Pattern
CORRECT: Well-Structured Express Server
// 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
// 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
// ❌ 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
// 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
// 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
// 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
// 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
// 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:
// 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
// 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
// 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
// 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: '<html>...</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('<html>...</html>'); }); 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
- ❌ No input validation
- ❌ Not using try/catch with async
- ❌ Business logic in route handlers
- ❌ Inconsistent error responses
- ❌ Missing CORS configuration
- ❌ Hard-coded configuration values
- ❌ No request logging
- ❌ Missing rate limiting
- ❌ Not using middleware for common tasks
- ❌ Ignoring security best practices