Agent skill
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 this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/express-api-patterns-bpmstc-websiteagent-2
SKILL.md
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
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?