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.md
source content

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

// 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

  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