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.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/express-api-patterns
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
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: '<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
Didn't find tool you were looking for?