
Senior Backend
- 248 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
senior-backend is an agent skill that designs, scaffolds, hardens, and load-tests Node-style backends with PostgreSQL-aware patterns for developers building or reviewing production APIs and services.
About
senior-backend is a borghei claude-skills module (version 1.0.0, updated 2026-03-31) for Node.js backend engineering with PostgreSQL. The skill activates for REST API design, query optimization, authentication, microservices, GraphQL setup, database migrations, load testing, and backend code review across Express and Fastify stacks. Manifest tags span api-design, microservices, databases, caching, and queues, signaling coverage from schema tuning through distributed service boundaries. Developers reach for senior-backend when scaffolding a new API, hardening auth and data access, optimizing slow PostgreSQL queries, or reviewing backend architecture before production traffic. The skill supports both greenfield service design and review passes on existing Node backends where security, performance, and migration safety need senior-level scrutiny.
- Covers API design, database optimization, and security hardening workflows with reference tables
- Three bundled tools: API Scaffolder, Database Migration Tool, and API Load Tester
- OpenAPI-driven route generation for Express-style frameworks via api_scaffolder.py
- Triggers on REST design, query tuning, auth, migrations, GraphQL setup, and backend code review
- Documents microservices, caching, and queue patterns for production-shaped APIs
Senior Backend by the numbers
- 248 all-time installs (skills.sh)
- Ranked #1,549 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/borghei/claude-skills --skill senior-backendAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 248 |
|---|---|
| repo stars | ★ 451 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
How do you design and harden Node PostgreSQL APIs?
Design, scaffold, harden, and load-test Node-style backends with PostgreSQL-aware patterns when you are building or reviewing APIs solo.
Who is it for?
Backend developers building or reviewing Node.js Express or Fastify APIs backed by PostgreSQL who need architecture, security, and performance patterns.
Skip if: Developers working only in frontend UI, non-Node languages without a Node migration, or tasks limited to a single trivial endpoint change.
When should I use this skill?
The user asks to design REST APIs, optimize PostgreSQL queries, implement authentication, set up GraphQL, run migrations, load test APIs, or review backend code.
What you get
Scaffolded or reviewed Node backend with API design, PostgreSQL tuning, auth, migrations, caching, and load-test guidance.
- API architecture
- optimized queries
- auth and migration plan
By the numbers
- Skill version 1.0.0 updated 2026-03-31
- Manifest lists 5 domain tags: api-design, microservices, databases, caching, queues
Files
Senior Backend Engineer
Scaffold and review backend services: API design and OpenAPI-driven code generation for Express/Fastify/Koa, PostgreSQL schema analysis and migration generation, HTTP load testing, and production security hardening. Outputs ready-to-run route handlers, Zod validators, TypeScript types, migrations with rollbacks, and load-test reports.
Core Capabilities
- API scaffolding — generate route handlers, validation middleware, TypeScript types, and OpenAPI specs across Express, Fastify, and Koa.
- Database optimization — schema analysis, missing-index detection, N+1 risk detection, and migration generation with paired rollback scripts.
- Load testing — configurable concurrency with latency percentiles (P50/P90/P95/P99), throughput, error rates, and endpoint comparison.
- Security hardening — JWT config, rate limiting, input validation (Zod), and security headers (helmet) for production readiness.
- Standardized contracts — consistent
data/error/metaresponse envelope and HTTP status conventions.
When to Use
- Designing a new API or refactoring existing endpoints.
- Slow queries or database performance needs improvement.
- Preparing an API for production or after a security review.
- Building regression/load-test baselines for backend endpoints.
Clarify First
Before scaffolding, confirm these inputs. If any is unknown or vague, ASK — do not assume:
- [ ] Framework — Express / Fastify / Koa (
--framework; changes the generated route handlers, validators, and types) - [ ] API contract source — the OpenAPI spec or endpoint list to scaffold from (the input the scaffolder reads)
- [ ] Database intent — the schema file and whether you want analysis vs migration generation (drives
database_migration_tool.py)
Stop rule: ask only the 2-3 that most change the output. If the user says "just draft it," proceed and list your assumptions at the top of the artifact.
Tools
| Tool | Purpose | Command |
|---|---|---|
api_scaffolder.py | Generate route handlers, Zod validators, and TS types from an OpenAPI spec | python scripts/api_scaffolder.py openapi.yaml --framework express --output src/routes/ |
database_migration_tool.py | Analyze schemas, suggest indexes, and generate migrations with rollbacks | python scripts/database_migration_tool.py schema.sql --analyze |
api_load_tester.py | HTTP load test with latency percentiles, throughput, and comparison | python scripts/api_load_tester.py https://api.example.com/users --concurrency 50 --duration 30 |
References
Load the reference that matches the task — keep this file lean and pull detail on demand:
- [references/tools-reference.md](references/tools-reference.md) — full usage examples, flag tables, and sample output for the scaffolder, migration tool, and load tester, plus quick-start and common commands. Read when running any tool.
- [references/workflows-and-patterns.md](references/workflows-and-patterns.md) — the API-design, database-optimization, and security-hardening workflows, common response/index patterns, the troubleshooting table, and the success-criteria bar. Read when designing or hardening a service.
- [references/api_design_patterns.md](references/api_design_patterns.md) — REST vs GraphQL, versioning, error handling, pagination. Read when designing new APIs.
- [references/database_optimization_guide.md](references/database_optimization_guide.md) — indexing strategies, query optimization, N+1 solutions. Read when fixing slow queries.
- [references/backend_security_practices.md](references/backend_security_practices.md) — OWASP Top 10, auth patterns, input validation. Read when hardening security.
Scope & Limitations
What this skill covers:
- REST API design, scaffolding, and OpenAPI-driven code generation for Express, Fastify, and Koa
- PostgreSQL schema analysis, index optimization, migration generation with rollback support
- HTTP load testing with latency percentile analysis, throughput measurement, and endpoint comparison
- Backend security patterns including JWT configuration, rate limiting, input validation, and security headers
What this skill does NOT cover:
- Frontend development, UI components, or client-side state management -- see
senior-frontend - Infrastructure provisioning, container orchestration, or CI/CD pipeline setup -- see
senior-devops - GraphQL schema design, resolvers, or subscriptions -- see
senior-fullstack - Application performance monitoring (APM), distributed tracing, or log aggregation -- see
senior-secops
Integration Points
| Skill | Integration | Data Flow |
|---|---|---|
senior-fullstack | API routes generated here feed into fullstack project scaffolding | OpenAPI spec → fullstack scaffolder consumes as API contract |
senior-devops | Migration scripts output here are consumed by CI/CD deployment pipelines | migrations/ directory → deployment workflow applies and verifies |
senior-security | Load test results and security hardening output feed into security review | Load test JSON → security audit validates rate limiting and error handling |
senior-qa | Generated route handlers and validators provide test surface for QA automation | Route files + Zod schemas → QA generates integration test suites |
senior-frontend | TypeScript types generated by the scaffolder are shared with frontend consumers | types.ts → frontend imports API types for type-safe client code |
code-reviewer | Schema analysis issues and migration diffs feed into code review checklists | Analysis report → reviewer validates index coverage and naming conventions |
API Design Patterns
Concrete patterns for REST and GraphQL API design with examples.
Patterns Index
1. REST vs GraphQL Decision 2. Resource Naming Conventions 3. API Versioning Strategies 4. Error Handling Patterns 5. Pagination Patterns 6. Authentication Patterns 7. Rate Limiting Design 8. Idempotency Patterns
---
1. REST vs GraphQL Decision
When to Use REST
| Scenario | Why REST |
|---|---|
| Simple CRUD operations | Less complexity, widely understood |
| Public APIs | Better caching, easier documentation |
| File uploads/downloads | Native HTTP support |
| Microservices communication | Simpler service-to-service calls |
| Caching is critical | HTTP caching built-in |
When to Use GraphQL
| Scenario | Why GraphQL |
|---|---|
| Mobile apps with bandwidth constraints | Request only needed fields |
| Complex nested data | Single request for related data |
| Rapidly changing frontend requirements | Frontend-driven queries |
| Multiple client types | Each client queries what it needs |
| Real-time subscriptions needed | Built-in subscription support |
Hybrid Approach
┌─────────────────────────────────────────────────────┐
│ API Gateway │
├─────────────────────────────────────────────────────┤
│ /api/v1/* → REST (Public API, webhooks) │
│ /graphql → GraphQL (Mobile apps, dashboards) │
│ /files/* → REST (File uploads/downloads) │
└─────────────────────────────────────────────────────┘---
2. Resource Naming Conventions
REST Endpoint Patterns
# Collections (plural nouns)
GET /users # List users
POST /users # Create user
GET /users/{id} # Get user
PUT /users/{id} # Replace user
PATCH /users/{id} # Update user
DELETE /users/{id} # Delete user
# Nested resources
GET /users/{id}/orders # User's orders
POST /users/{id}/orders # Create order for user
GET /users/{id}/orders/{orderId} # Specific order
# Actions (when CRUD doesn't fit)
POST /users/{id}/activate # Activate user
POST /orders/{id}/cancel # Cancel order
POST /payments/{id}/refund # Refund payment
# Filtering, sorting, pagination
GET /users?status=active&sort=-created_at&limit=20&offset=40
GET /orders?user_id=123&status=pendingNaming Rules
| Rule | Good | Bad |
|---|---|---|
| Use plural nouns | /users | /user |
| Use lowercase | /user-profiles | /userProfiles |
| Use hyphens | /order-items | /order_items |
| No verbs in URLs | POST /orders | POST /createOrder |
| No file extensions | /users/123 | /users/123.json |
---
3. API Versioning Strategies
Strategy Comparison
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL Path | /api/v1/users | Explicit, easy routing | URL changes |
| Header | Accept: application/vnd.api+json;version=1 | Clean URLs | Hidden version |
| Query Param | /users?version=1 | Easy to test | Pollutes query string |
Recommended: URL Path Versioning
// Express routing
import v1Routes from './routes/v1';
import v2Routes from './routes/v2';
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);Deprecation Strategy
// Add deprecation headers
app.use('/api/v1', (req, res, next) => {
res.set('Deprecation', 'true');
res.set('Sunset', 'Sat, 01 Jun 2025 00:00:00 GMT');
res.set('Link', '</api/v2>; rel="successor-version"');
next();
}, v1Routes);Breaking vs Non-Breaking Changes
Non-breaking (safe):
- Adding new endpoints
- Adding optional fields
- Adding new enum values at end
Breaking (requires new version):
- Removing endpoints or fields
- Renaming fields
- Changing field types
- Changing required/optional status
---
4. Error Handling Patterns
Standard Error Response Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Must be a valid email address"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Must be between 18 and 120"
}
],
"documentation_url": "https://api.example.com/docs/errors#validation"
},
"meta": {
"request_id": "req_abc123",
"timestamp": "2024-01-15T10:30:00Z"
}
}Error Codes by Category
// Client errors (4xx)
const ClientErrors = {
VALIDATION_ERROR: 400,
INVALID_JSON: 400,
AUTHENTICATION_REQUIRED: 401,
INVALID_TOKEN: 401,
TOKEN_EXPIRED: 401,
PERMISSION_DENIED: 403,
RESOURCE_NOT_FOUND: 404,
METHOD_NOT_ALLOWED: 405,
CONFLICT: 409,
RATE_LIMIT_EXCEEDED: 429,
};
// Server errors (5xx)
const ServerErrors = {
INTERNAL_ERROR: 500,
DATABASE_ERROR: 500,
EXTERNAL_SERVICE_ERROR: 502,
SERVICE_UNAVAILABLE: 503,
};Error Handler Implementation
// Express error handler
interface ApiError extends Error {
code: string;
statusCode: number;
details?: Array<{ field: string; message: string }>;
}
const errorHandler: ErrorRequestHandler = (err: ApiError, req, res, next) => {
const statusCode = err.statusCode || 500;
const code = err.code || 'INTERNAL_ERROR';
// Log server errors
if (statusCode >= 500) {
logger.error({ err, requestId: req.id }, 'Server error');
}
res.status(statusCode).json({
error: {
code,
message: statusCode >= 500 ? 'An unexpected error occurred' : err.message,
details: err.details,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
},
meta: {
request_id: req.id,
timestamp: new Date().toISOString(),
},
});
};---
5. Pagination Patterns
Offset-Based Pagination
GET /users?limit=20&offset=40
Response:
{
"data": [...],
"pagination": {
"total": 1250,
"limit": 20,
"offset": 40,
"has_more": true
}
}Pros: Simple, supports random access Cons: Inconsistent with concurrent inserts/deletes
Cursor-Based Pagination
GET /users?limit=20&cursor=eyJpZCI6MTIzfQ==
Response:
{
"data": [...],
"pagination": {
"limit": 20,
"next_cursor": "eyJpZCI6MTQzfQ==",
"prev_cursor": "eyJpZCI6MTIzfQ==",
"has_more": true
}
}Pros: Consistent with real-time data, efficient Cons: No random access, cursor encoding required
Implementation Example
// Cursor-based pagination
interface CursorPagination {
limit: number;
cursor?: string;
direction?: 'forward' | 'backward';
}
async function paginatedQuery<T>(
query: QueryBuilder,
{ limit, cursor, direction = 'forward' }: CursorPagination
): Promise<{ data: T[]; nextCursor?: string; hasMore: boolean }> {
// Decode cursor
const decoded = cursor ? JSON.parse(Buffer.from(cursor, 'base64').toString()) : null;
// Apply cursor condition
if (decoded) {
query = direction === 'forward'
? query.where('id', '>', decoded.id)
: query.where('id', '<', decoded.id);
}
// Fetch one extra to check if more exist
const results = await query.limit(limit + 1).orderBy('id', direction === 'forward' ? 'asc' : 'desc');
const hasMore = results.length > limit;
const data = hasMore ? results.slice(0, -1) : results;
// Encode next cursor
const nextCursor = hasMore
? Buffer.from(JSON.stringify({ id: data[data.length - 1].id })).toString('base64')
: undefined;
return { data, nextCursor, hasMore };
}---
6. Authentication Patterns
JWT Authentication Flow
┌──────────┐ 1. Login ┌──────────┐
│ Client │ ──────────────────▶ │ Server │
└──────────┘ └──────────┘
│
2. Return JWT │
◀────────────────────────────────────────
{access_token, refresh_token} │
│
3. API Request │
───────────────────────────────────────▶
Authorization: Bearer {token} │
│
4. Validate & Respond │
◀────────────────────────────────────────JWT Implementation
import jwt from 'jsonwebtoken';
interface TokenPayload {
userId: string;
email: string;
roles: string[];
}
// Generate tokens
function generateTokens(user: User): { accessToken: string; refreshToken: string } {
const payload: TokenPayload = {
userId: user.id,
email: user.email,
roles: user.roles,
};
const accessToken = jwt.sign(payload, process.env.JWT_SECRET!, {
expiresIn: '15m',
algorithm: 'RS256',
});
const refreshToken = jwt.sign(
{ userId: user.id, tokenVersion: user.tokenVersion },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d', algorithm: 'RS256' }
);
return { accessToken, refreshToken };
}
// Middleware
const authenticate: RequestHandler = async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: { code: 'AUTHENTICATION_REQUIRED' } });
}
try {
const token = authHeader.slice(7);
const payload = jwt.verify(token, process.env.JWT_SECRET!) as TokenPayload;
req.user = payload;
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
return res.status(401).json({ error: { code: 'TOKEN_EXPIRED' } });
}
return res.status(401).json({ error: { code: 'INVALID_TOKEN' } });
}
};API Key Authentication (Service-to-Service)
// API key middleware
const apiKeyAuth: RequestHandler = async (req, res, next) => {
const apiKey = req.headers['x-api-key'] as string;
if (!apiKey) {
return res.status(401).json({ error: { code: 'API_KEY_REQUIRED' } });
}
// Hash and lookup (never store plain API keys)
const hashedKey = crypto.createHash('sha256').update(apiKey).digest('hex');
const client = await db.apiClients.findByHashedKey(hashedKey);
if (!client || !client.isActive) {
return res.status(401).json({ error: { code: 'INVALID_API_KEY' } });
}
req.apiClient = client;
next();
};---
7. Rate Limiting Design
Rate Limit Headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705312800
Retry-After: 60Tiered Rate Limits
const rateLimits = {
anonymous: { requests: 60, window: '1m' },
authenticated: { requests: 1000, window: '1h' },
premium: { requests: 10000, window: '1h' },
};
// Implementation with Redis
import { RateLimiterRedis } from 'rate-limiter-flexible';
const createRateLimiter = (tier: keyof typeof rateLimits) => {
const config = rateLimits[tier];
return new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: `ratelimit:${tier}`,
points: config.requests,
duration: parseDuration(config.window),
});
};Rate Limit Response
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests",
"details": {
"limit": 100,
"window": "1 minute",
"retry_after": 45
}
}
}---
8. Idempotency Patterns
Idempotency Key Header
POST /payments
Idempotency-Key: payment_abc123_attempt1
Content-Type: application/json
{
"amount": 1000,
"currency": "USD"
}Implementation
const idempotencyMiddleware: RequestHandler = async (req, res, next) => {
const idempotencyKey = req.headers['idempotency-key'] as string;
if (!idempotencyKey) {
return next(); // Optional for some endpoints
}
// Check for existing response
const cached = await redis.get(`idempotency:${idempotencyKey}`);
if (cached) {
const { statusCode, body } = JSON.parse(cached);
return res.status(statusCode).json(body);
}
// Store response after processing
const originalJson = res.json.bind(res);
res.json = (body: any) => {
redis.setex(
`idempotency:${idempotencyKey}`,
86400, // 24 hours
JSON.stringify({ statusCode: res.statusCode, body })
);
return originalJson(body);
};
next();
};---
Quick Reference: HTTP Methods
| Method | Idempotent | Safe | Cacheable | Request Body |
|---|---|---|---|---|
| GET | Yes | Yes | Yes | No |
| HEAD | Yes | Yes | Yes | No |
| POST | No | No | Conditional | Yes |
| PUT | Yes | No | No | Yes |
| PATCH | No | No | No | Yes |
| DELETE | Yes | No | No | Optional |
| OPTIONS | Yes | Yes | No | No |
Backend Security Practices
Security patterns and OWASP Top 10 mitigations for Node.js/Express applications.
Guide Index
1. OWASP Top 10 Mitigations 2. Input Validation 3. SQL Injection Prevention 4. XSS Prevention 5. Authentication Security 6. Authorization Patterns 7. Security Headers 8. Secrets Management 9. Logging and Monitoring
---
1. OWASP Top 10 Mitigations
A01: Broken Access Control
// BAD: Direct object reference
app.get('/users/:id/profile', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user); // Anyone can access any user!
});
// GOOD: Verify ownership
app.get('/users/:id/profile', authenticate, async (req, res) => {
const userId = req.params.id;
// Verify user can only access their own data
if (req.user.id !== userId && !req.user.roles.includes('admin')) {
return res.status(403).json({ error: { code: 'FORBIDDEN' } });
}
const user = await db.users.findById(userId);
res.json(user);
});A02: Cryptographic Failures
// BAD: Weak hashing
const hash = crypto.createHash('md5').update(password).digest('hex');
// GOOD: bcrypt with appropriate cost factor
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12; // Adjust based on hardware
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}A03: Injection
// BAD: String concatenation in SQL
const query = `SELECT * FROM users WHERE email = '${email}'`;
// GOOD: Parameterized queries
const result = await db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);A04: Insecure Design
// BAD: No rate limiting on sensitive operations
app.post('/forgot-password', async (req, res) => {
await sendResetEmail(req.body.email);
res.json({ message: 'If email exists, reset link sent' });
});
// GOOD: Rate limit + consistent response time
import rateLimit from 'express-rate-limit';
const passwordResetLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 3, // 3 attempts per 15 minutes
skipSuccessfulRequests: false,
});
app.post('/forgot-password', passwordResetLimiter, async (req, res) => {
const startTime = Date.now();
try {
const user = await db.users.findByEmail(req.body.email);
if (user) {
await sendResetEmail(user.email);
}
} catch (err) {
logger.error(err);
}
// Consistent response time prevents timing attacks
const elapsed = Date.now() - startTime;
const minDelay = 500;
if (elapsed < minDelay) {
await sleep(minDelay - elapsed);
}
// Same response regardless of email existence
res.json({ message: 'If email exists, reset link sent' });
});A05: Security Misconfiguration
// BAD: Detailed errors in production
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message,
stack: err.stack, // Exposes internals!
});
});
// GOOD: Environment-aware error handling
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const requestId = req.id;
// Always log full error internally
logger.error({ err, requestId }, 'Unhandled error');
// Return safe response
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: process.env.NODE_ENV === 'development'
? err.message
: 'An unexpected error occurred',
requestId,
},
});
});A06: Vulnerable Components
# Check for vulnerabilities
npm audit
# Fix automatically where possible
npm audit fix
# Check specific package
npm audit --package-lock-only
# Use Snyk for deeper analysis
npx snyk test// Automated dependency updates (package.json)
{
"scripts": {
"security:audit": "npm audit --audit-level=high",
"security:check": "snyk test",
"preinstall": "npm audit"
}
}A07: Authentication Failures
// BAD: Weak session management
app.post('/login', async (req, res) => {
const user = await authenticate(req.body);
req.session.userId = user.id; // Session fixation risk
res.json({ success: true });
});
// GOOD: Regenerate session on authentication
app.post('/login', async (req, res) => {
const user = await authenticate(req.body);
// Regenerate session to prevent fixation
req.session.regenerate((err) => {
if (err) return next(err);
req.session.userId = user.id;
req.session.createdAt = Date.now();
req.session.save((err) => {
if (err) return next(err);
res.json({ success: true });
});
});
});A08: Software and Data Integrity Failures
// Verify webhook signatures (e.g., Stripe)
import Stripe from 'stripe';
app.post('/webhooks/stripe',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['stripe-signature'] as string;
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
endpointSecret
);
} catch (err) {
logger.warn({ err }, 'Webhook signature verification failed');
return res.status(400).json({ error: 'Invalid signature' });
}
// Process verified event
await handleStripeEvent(event);
res.json({ received: true });
}
);A09: Security Logging Failures
// Comprehensive security logging
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
redact: ['req.headers.authorization', 'req.body.password'], // Redact sensitive
});
// Log security events
function logSecurityEvent(event: {
type: 'LOGIN_SUCCESS' | 'LOGIN_FAILURE' | 'ACCESS_DENIED' | 'SUSPICIOUS_ACTIVITY';
userId?: string;
ip: string;
userAgent: string;
details?: Record<string, unknown>;
}) {
logger.info({
security: true,
...event,
timestamp: new Date().toISOString(),
}, `Security event: ${event.type}`);
}
// Usage
app.post('/login', async (req, res) => {
try {
const user = await authenticate(req.body);
logSecurityEvent({
type: 'LOGIN_SUCCESS',
userId: user.id,
ip: req.ip,
userAgent: req.headers['user-agent'] || '',
});
// ...
} catch (err) {
logSecurityEvent({
type: 'LOGIN_FAILURE',
ip: req.ip,
userAgent: req.headers['user-agent'] || '',
details: { email: req.body.email },
});
// ...
}
});A10: Server-Side Request Forgery (SSRF)
// BAD: Unvalidated URL fetch
app.post('/fetch-url', async (req, res) => {
const response = await fetch(req.body.url); // SSRF vulnerability!
res.json({ data: await response.text() });
});
// GOOD: URL allowlist and validation
import { URL } from 'url';
const ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com'];
function isAllowedUrl(urlString: string): boolean {
try {
const url = new URL(urlString);
// Block internal IPs
const blockedPatterns = [
/^localhost$/i,
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
/^192\.168\./,
/^0\./,
/^169\.254\./,
/^\[::1\]$/,
/^metadata\.google\.internal$/,
/^169\.254\.169\.254$/,
];
if (blockedPatterns.some(p => p.test(url.hostname))) {
return false;
}
// Only allow HTTPS
if (url.protocol !== 'https:') {
return false;
}
// Check allowlist
return ALLOWED_HOSTS.includes(url.hostname);
} catch {
return false;
}
}
app.post('/fetch-url', async (req, res) => {
const { url } = req.body;
if (!isAllowedUrl(url)) {
return res.status(400).json({ error: { code: 'INVALID_URL' } });
}
const response = await fetch(url, {
timeout: 5000,
follow: 0, // Don't follow redirects
});
res.json({ data: await response.text() });
});---
2. Input Validation
Schema Validation with Zod
import { z } from 'zod';
// Define schemas
const CreateUserSchema = z.object({
email: z.string().email().max(255).toLowerCase(),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.max(72, 'Password must be at most 72 characters') // bcrypt limit
.regex(/[A-Z]/, 'Password must contain uppercase letter')
.regex(/[a-z]/, 'Password must contain lowercase letter')
.regex(/[0-9]/, 'Password must contain number'),
name: z.string().min(1).max(100).trim(),
age: z.number().int().min(18).max(120).optional(),
});
const PaginationSchema = z.object({
limit: z.coerce.number().int().min(1).max(100).default(20),
offset: z.coerce.number().int().min(0).default(0),
sort: z.enum(['asc', 'desc']).default('desc'),
});
// Validation middleware
function validate<T>(schema: z.ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
const details = result.error.errors.map(err => ({
field: err.path.join('.'),
code: err.code,
message: err.message,
}));
return res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Request validation failed',
details,
},
});
}
req.body = result.data;
next();
};
}
// Usage
app.post('/users', validate(CreateUserSchema), async (req, res) => {
// req.body is now typed and validated
const user = await userService.create(req.body);
res.status(201).json(user);
});Sanitization
import DOMPurify from 'isomorphic-dompurify';
import xss from 'xss';
// HTML sanitization for rich text fields
function sanitizeHtml(dirty: string): string {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href'],
});
}
// Plain text sanitization (strip all HTML)
function sanitizePlainText(dirty: string): string {
return xss(dirty, {
whiteList: {},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script'],
});
}
// File path sanitization
import path from 'path';
function sanitizePath(userPath: string, baseDir: string): string | null {
const resolved = path.resolve(baseDir, userPath);
// Prevent directory traversal
if (!resolved.startsWith(baseDir)) {
return null;
}
return resolved;
}---
3. SQL Injection Prevention
Parameterized Queries
// BAD: String interpolation
const email = "'; DROP TABLE users; --";
db.query(`SELECT * FROM users WHERE email = '${email}'`);
// GOOD: Parameterized query (pg)
const result = await db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
// GOOD: Parameterized query (mysql2)
const [rows] = await connection.execute(
'SELECT * FROM users WHERE email = ?',
[email]
);Query Builders
// Using Knex.js
const users = await knex('users')
.where('email', email) // Automatically parameterized
.andWhere('status', 'active')
.select('id', 'name', 'email');
// Dynamic WHERE with safe column names
const ALLOWED_COLUMNS = ['name', 'email', 'created_at'] as const;
function buildUserQuery(filters: Record<string, string>) {
let query = knex('users').select('id', 'name', 'email');
for (const [column, value] of Object.entries(filters)) {
// Validate column name against allowlist
if (ALLOWED_COLUMNS.includes(column as any)) {
query = query.where(column, value);
}
}
return query;
}ORM Safety
// Prisma (safe by default)
const user = await prisma.user.findUnique({
where: { email }, // Automatically escaped
});
// TypeORM (safe by default)
const user = await userRepository.findOne({
where: { email }, // Automatically escaped
});
// DANGER: Raw queries still require parameterization
// BAD
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);
// GOOD
await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;---
4. XSS Prevention
Output Encoding
// Server-side template rendering (EJS)
// In template: <%= userInput %> (escaped)
// NOT: <%- userInput %> (raw, dangerous)
// Manual HTML encoding
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// JSON response (automatically safe in modern frameworks)
res.json({ message: userInput }); // JSON.stringify escapes by defaultContent Security Policy
import helmet from 'helmet';
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'strict-dynamic'"],
styleSrc: ["'self'", "'unsafe-inline'"], // Consider using nonces
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
}));API Response Safety
// Set correct Content-Type for JSON APIs
app.use((req, res, next) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
// Disable JSONP (if not needed)
// Don't implement callback parameter handling
// Safe JSON response
res.json({
data: sanitizedData,
// Never reflect raw user input
});---
5. Authentication Security
Password Storage
import bcrypt from 'bcrypt';
import { randomBytes } from 'crypto';
const SALT_ROUNDS = 12;
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// For password reset tokens
function generateSecureToken(): string {
return randomBytes(32).toString('hex');
}
// Token expiration (store in DB)
interface PasswordResetToken {
token: string; // Hashed
userId: string;
expiresAt: Date; // 1 hour from creation
}JWT Best Practices
import jwt from 'jsonwebtoken';
// Use asymmetric keys in production
const PRIVATE_KEY = process.env.JWT_PRIVATE_KEY!;
const PUBLIC_KEY = process.env.JWT_PUBLIC_KEY!;
interface AccessTokenPayload {
sub: string; // User ID
email: string;
roles: string[];
iat: number;
exp: number;
}
function generateAccessToken(user: User): string {
const payload: Omit<AccessTokenPayload, 'iat' | 'exp'> = {
sub: user.id,
email: user.email,
roles: user.roles,
};
return jwt.sign(payload, PRIVATE_KEY, {
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'api.example.com',
audience: 'example.com',
});
}
function verifyAccessToken(token: string): AccessTokenPayload {
return jwt.verify(token, PUBLIC_KEY, {
algorithms: ['RS256'],
issuer: 'api.example.com',
audience: 'example.com',
}) as AccessTokenPayload;
}
// Refresh tokens should be stored in DB and rotated
interface RefreshToken {
id: string;
token: string; // Hashed
userId: string;
expiresAt: Date;
family: string; // For rotation detection
isRevoked: boolean;
}Session Management
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: process.env.REDIS_URL });
app.use(session({
store: new RedisStore({ client: redisClient }),
name: 'sessionId', // Don't use default 'connect.sid'
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
domain: process.env.COOKIE_DOMAIN,
},
}));
// Regenerate session on privilege change
async function elevateSession(req: Request): Promise<void> {
return new Promise((resolve, reject) => {
const userId = req.session.userId;
req.session.regenerate((err) => {
if (err) return reject(err);
req.session.userId = userId;
req.session.elevated = true;
req.session.elevatedAt = Date.now();
resolve();
});
});
}---
6. Authorization Patterns
Role-Based Access Control (RBAC)
type Role = 'user' | 'moderator' | 'admin';
type Permission = 'read:users' | 'write:users' | 'delete:users' | 'read:admin';
const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
user: ['read:users'],
moderator: ['read:users', 'write:users'],
admin: ['read:users', 'write:users', 'delete:users', 'read:admin'],
};
function hasPermission(userRoles: Role[], required: Permission): boolean {
return userRoles.some(role =>
ROLE_PERMISSIONS[role]?.includes(required)
);
}
// Middleware
function requirePermission(permission: Permission) {
return (req: Request, res: Response, next: NextFunction) => {
if (!hasPermission(req.user.roles, permission)) {
return res.status(403).json({
error: { code: 'FORBIDDEN', message: 'Insufficient permissions' },
});
}
next();
};
}
// Usage
app.delete('/users/:id',
authenticate,
requirePermission('delete:users'),
deleteUserHandler
);Attribute-Based Access Control (ABAC)
interface AccessContext {
user: { id: string; roles: string[]; department: string };
resource: { ownerId: string; department: string; sensitivity: string };
action: 'read' | 'write' | 'delete';
environment: { time: Date; ip: string };
}
interface Policy {
name: string;
condition: (ctx: AccessContext) => boolean;
}
const policies: Policy[] = [
{
name: 'owner-full-access',
condition: (ctx) => ctx.resource.ownerId === ctx.user.id,
},
{
name: 'same-department-read',
condition: (ctx) =>
ctx.action === 'read' &&
ctx.resource.department === ctx.user.department,
},
{
name: 'admin-override',
condition: (ctx) => ctx.user.roles.includes('admin'),
},
{
name: 'no-sensitive-outside-hours',
condition: (ctx) => {
const hour = ctx.environment.time.getHours();
return ctx.resource.sensitivity !== 'high' || (hour >= 9 && hour <= 17);
},
},
];
function evaluateAccess(ctx: AccessContext): boolean {
return policies.some(policy => policy.condition(ctx));
}---
7. Security Headers
Complete Helmet Configuration
import helmet from 'helmet';
app.use(helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'none'"],
frameSrc: ["'none'"],
},
},
// Strict Transport Security
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
// Prevent clickjacking
frameguard: { action: 'deny' },
// Prevent MIME sniffing
noSniff: true,
// XSS filter (legacy browsers)
xssFilter: true,
// Hide X-Powered-By
hidePoweredBy: true,
// Referrer policy
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
// Cross-origin policies
crossOriginEmbedderPolicy: false, // Enable if using SharedArrayBuffer
crossOriginOpenerPolicy: { policy: 'same-origin' },
crossOriginResourcePolicy: { policy: 'same-origin' },
}));
// CORS configuration
import cors from 'cors';
app.use(cors({
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400, // 24 hours
}));Header Reference
| Header | Purpose | Value |
|---|---|---|
Strict-Transport-Security | Force HTTPS | max-age=31536000; includeSubDomains; preload |
Content-Security-Policy | Prevent XSS | See above |
X-Content-Type-Options | Prevent MIME sniffing | nosniff |
X-Frame-Options | Prevent clickjacking | DENY |
Referrer-Policy | Control referrer info | strict-origin-when-cross-origin |
Permissions-Policy | Feature restrictions | geolocation=(), microphone=() |
---
8. Secrets Management
Environment Variables
// config/secrets.ts
import { z } from 'zod';
const SecretsSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
JWT_PRIVATE_KEY: z.string(),
JWT_PUBLIC_KEY: z.string(),
REDIS_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
});
// Validate on startup
export const secrets = SecretsSchema.parse(process.env);
// NEVER log secrets
console.log('Config loaded:', {
database: secrets.DATABASE_URL.replace(/\/\/.*@/, '//***@'),
redis: 'configured',
stripe: 'configured',
});Secret Rotation
// Support multiple keys during rotation
const JWT_SECRETS = [
process.env.JWT_SECRET_CURRENT!,
process.env.JWT_SECRET_PREVIOUS!, // Keep for grace period
].filter(Boolean);
function verifyTokenWithRotation(token: string): TokenPayload | null {
for (const secret of JWT_SECRETS) {
try {
return jwt.verify(token, secret) as TokenPayload;
} catch {
continue;
}
}
return null;
}Vault Integration
import Vault from 'node-vault';
const vault = Vault({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN,
});
async function getSecret(path: string): Promise<string> {
const result = await vault.read(`secret/data/${path}`);
return result.data.data.value;
}
// Cache secrets with TTL
const secretsCache = new Map<string, { value: string; expiresAt: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function getCachedSecret(path: string): Promise<string> {
const cached = secretsCache.get(path);
if (cached && cached.expiresAt > Date.now()) {
return cached.value;
}
const value = await getSecret(path);
secretsCache.set(path, { value, expiresAt: Date.now() + CACHE_TTL });
return value;
}---
9. Logging and Monitoring
Security Event Logging
import pino from 'pino';
const logger = pino({
level: 'info',
redact: {
paths: [
'req.headers.authorization',
'req.headers.cookie',
'req.body.password',
'req.body.token',
'*.password',
'*.secret',
'*.apiKey',
],
censor: '[REDACTED]',
},
});
// Security event types
type SecurityEventType =
| 'AUTH_SUCCESS'
| 'AUTH_FAILURE'
| 'AUTH_LOCKOUT'
| 'PASSWORD_CHANGED'
| 'PASSWORD_RESET_REQUEST'
| 'PERMISSION_DENIED'
| 'RATE_LIMIT_EXCEEDED'
| 'SUSPICIOUS_ACTIVITY'
| 'TOKEN_REVOKED';
interface SecurityEvent {
type: SecurityEventType;
userId?: string;
ip: string;
userAgent: string;
path: string;
details?: Record<string, unknown>;
}
function logSecurityEvent(event: SecurityEvent): void {
logger.info({
security: true,
...event,
timestamp: new Date().toISOString(),
}, `Security: ${event.type}`);
}Request Logging
import pinoHttp from 'pino-http';
app.use(pinoHttp({
logger,
genReqId: (req) => req.headers['x-request-id'] || crypto.randomUUID(),
serializers: {
req: (req) => ({
id: req.id,
method: req.method,
url: req.url,
remoteAddress: req.remoteAddress,
// Don't log headers by default (may contain sensitive data)
}),
res: (res) => ({
statusCode: res.statusCode,
}),
},
customLogLevel: (req, res, err) => {
if (res.statusCode >= 500 || err) return 'error';
if (res.statusCode >= 400) return 'warn';
return 'info';
},
}));Alerting Thresholds
| Metric | Warning | Critical |
|---|---|---|
| Failed logins per IP (15 min) | > 5 | > 10 |
| Failed logins per account (1 hour) | > 3 | > 5 |
| 403 responses per IP (5 min) | > 10 | > 50 |
| 500 errors (5 min) | > 5 | > 20 |
| Request rate per IP (1 min) | > 100 | > 500 |
---
Quick Reference: Security Checklist
Authentication
- [ ] bcrypt with cost >= 12 for password hashing
- [ ] JWT with RS256, short expiry (15-30 min)
- [ ] Refresh token rotation with family detection
- [ ] Session regeneration on login
- [ ] Secure cookie flags (httpOnly, secure, sameSite)
Input Validation
- [ ] Schema validation on all inputs (Zod)
- [ ] Parameterized queries (never string concat)
- [ ] File path sanitization
- [ ] Content-Type validation
Headers
- [ ] Strict-Transport-Security
- [ ] Content-Security-Policy
- [ ] X-Content-Type-Options: nosniff
- [ ] X-Frame-Options: DENY
- [ ] CORS with specific origins
Logging
- [ ] Redact sensitive fields
- [ ] Log security events
- [ ] Include request IDs
- [ ] Alert on anomalies
Dependencies
- [ ] npm audit in CI
- [ ] Automated dependency updates
- [ ] Lock file committed
Database Optimization Guide
Practical strategies for PostgreSQL query optimization, indexing, and performance tuning.
Guide Index
1. Query Analysis with EXPLAIN 2. Indexing Strategies 3. N+1 Query Problem 4. Connection Pooling 5. Query Optimization Patterns 6. Database Migrations 7. Monitoring and Alerting
---
1. Query Analysis with EXPLAIN
Basic EXPLAIN Usage
-- Show query plan
EXPLAIN SELECT * FROM orders WHERE user_id = 123;
-- Show plan with actual execution times
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;
-- Show buffers and I/O statistics
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE user_id = 123;Reading EXPLAIN Output
QUERY PLAN
---------------------------------------------------------------------------
Index Scan using idx_orders_user_id on orders (cost=0.43..8.45 rows=10 width=120)
Index Cond: (user_id = 123)
Buffers: shared hit=3
Planning Time: 0.152 ms
Execution Time: 0.089 msKey metrics:
cost: Estimated cost (startup..total)rows: Estimated row countwidth: Average row size in bytesactual time: Real execution time (with ANALYZE)Buffers: shared hit: Pages read from cache
Scan Types (Best to Worst)
| Scan Type | Description | Performance |
|---|---|---|
| Index Only Scan | Data from index alone | Best |
| Index Scan | Index lookup + heap fetch | Good |
| Bitmap Index Scan | Multiple index conditions | Good |
| Index Scan + Filter | Index + row filtering | Okay |
| Seq Scan (small table) | Full table scan | Okay |
| Seq Scan (large table) | Full table scan | Bad |
| Nested Loop (large) | O(n*m) join | Very Bad |
Warning Signs
-- BAD: Sequential scan on large table
Seq Scan on orders (cost=0.00..1854231.00 rows=50000000 width=120)
Filter: (status = 'pending')
Rows Removed by Filter: 49500000
-- BAD: Nested loop with high iterations
Nested Loop (cost=0.43..2847593.20 rows=12500000 width=240)
-> Seq Scan on users (cost=0.00..1250.00 rows=50000 width=120)
-> Index Scan on orders (cost=0.43..45.73 rows=250 width=120)
Index Cond: (orders.user_id = users.id)---
2. Indexing Strategies
Index Types
-- B-tree (default, most common)
CREATE INDEX idx_users_email ON users(email);
-- Hash (equality only, rarely better than B-tree)
CREATE INDEX idx_users_id_hash ON users USING hash(id);
-- GIN (arrays, JSONB, full-text search)
CREATE INDEX idx_products_tags ON products USING gin(tags);
CREATE INDEX idx_users_data ON users USING gin(metadata jsonb_path_ops);
-- GiST (geometric, range types, full-text)
CREATE INDEX idx_locations_point ON locations USING gist(coordinates);Composite Indexes
-- Order matters! Column with = first, then range/sort
CREATE INDEX idx_orders_user_status_date
ON orders(user_id, status, created_at DESC);
-- This index supports:
-- WHERE user_id = ?
-- WHERE user_id = ? AND status = ?
-- WHERE user_id = ? AND status = ? ORDER BY created_at DESC
-- WHERE user_id = ? ORDER BY created_at DESC
-- This index does NOT efficiently support:
-- WHERE status = ? (user_id not in query)
-- WHERE created_at > ? (leftmost column not in query)Partial Indexes
-- Index only active users (smaller, faster)
CREATE INDEX idx_users_active_email
ON users(email)
WHERE status = 'active';
-- Index only recent orders
CREATE INDEX idx_orders_recent
ON orders(created_at DESC)
WHERE created_at > CURRENT_DATE - INTERVAL '90 days';
-- Index only unprocessed items
CREATE INDEX idx_queue_pending
ON job_queue(priority DESC, created_at)
WHERE processed_at IS NULL;Covering Indexes (Index-Only Scans)
-- Include non-indexed columns to avoid heap lookup
CREATE INDEX idx_users_email_covering
ON users(email)
INCLUDE (name, created_at);
-- Query can be satisfied from index alone
SELECT name, created_at FROM users WHERE email = 'test@example.com';
-- Result: Index Only ScanIndex Maintenance
-- Check index usage
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;
-- Find unused indexes (candidates for removal)
SELECT indexrelid::regclass as index,
relid::regclass as table,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND indexrelid NOT IN (SELECT conindid FROM pg_constraint);
-- Rebuild bloated indexes
REINDEX INDEX CONCURRENTLY idx_orders_user_id;---
3. N+1 Query Problem
The Problem
// BAD: N+1 queries
const users = await db.query('SELECT * FROM users LIMIT 100');
for (const user of users) {
// This runs 100 times!
const orders = await db.query(
'SELECT * FROM orders WHERE user_id = $1',
[user.id]
);
user.orders = orders;
}
// Total queries: 1 + 100 = 101Solution 1: JOIN
// GOOD: Single query with JOIN
const usersWithOrders = await db.query(`
SELECT u.*, o.id as order_id, o.total, o.status
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
LIMIT 100
`);
// Total queries: 1Solution 2: Batch Loading (DataLoader pattern)
// GOOD: Two queries with batch loading
const users = await db.query('SELECT * FROM users LIMIT 100');
const userIds = users.map(u => u.id);
const orders = await db.query(
'SELECT * FROM orders WHERE user_id = ANY($1)',
[userIds]
);
// Group orders by user_id
const ordersByUser = groupBy(orders, 'user_id');
users.forEach(user => {
user.orders = ordersByUser[user.id] || [];
});
// Total queries: 2Solution 3: ORM Eager Loading
// Prisma
const users = await prisma.user.findMany({
take: 100,
include: { orders: true }
});
// TypeORM
const users = await userRepository.find({
take: 100,
relations: ['orders']
});
// Sequelize
const users = await User.findAll({
limit: 100,
include: [{ model: Order }]
});Detecting N+1 in Production
// Query logging middleware
let queryCount = 0;
const originalQuery = db.query;
db.query = async (...args) => {
queryCount++;
if (queryCount > 10) {
console.warn(`High query count: ${queryCount} in single request`);
console.trace();
}
return originalQuery.apply(db, args);
};---
4. Connection Pooling
Why Pooling Matters
Without pooling:
Request → Create connection → Query → Close connection
(50-100ms overhead)
With pooling:
Request → Get connection from pool → Query → Return to pool
(0-1ms overhead)pg-pool Configuration
import { Pool } from 'pg';
const pool = new Pool({
host: process.env.DB_HOST,
port: 5432,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
// Pool settings
min: 5, // Minimum connections
max: 20, // Maximum connections
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 5000, // Fail if can't connect in 5s
// Statement timeout (cancel long queries)
statement_timeout: 30000,
});
// Health check
pool.on('error', (err, client) => {
console.error('Unexpected pool error', err);
});Pool Sizing Formula
Optimal connections = (CPU cores * 2) + effective_spindle_count
For SSD with 4 cores:
connections = (4 * 2) + 1 = 9
For multiple app servers:
connections_per_server = total_connections / num_serversPgBouncer for High Scale
# pgbouncer.ini
[databases]
mydb = host=localhost port=5432 dbname=mydb
[pgbouncer]
listen_port = 6432
listen_addr = 0.0.0.0
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
reserve_pool_size = 5---
5. Query Optimization Patterns
Pagination Optimization
-- BAD: OFFSET is slow for large values
SELECT * FROM orders ORDER BY created_at DESC LIMIT 20 OFFSET 10000;
-- Must scan 10,020 rows, discard 10,000
-- GOOD: Cursor-based pagination
SELECT * FROM orders
WHERE created_at < '2024-01-15T10:00:00Z'
ORDER BY created_at DESC
LIMIT 20;
-- Only scans 20 rowsBatch Updates
-- BAD: Individual updates
UPDATE orders SET status = 'shipped' WHERE id = 1;
UPDATE orders SET status = 'shipped' WHERE id = 2;
-- ...repeat 1000 times
-- GOOD: Batch update
UPDATE orders
SET status = 'shipped'
WHERE id = ANY(ARRAY[1, 2, 3, ...1000]);
-- GOOD: Update from values
UPDATE orders o
SET status = v.new_status
FROM (VALUES
(1, 'shipped'),
(2, 'delivered'),
(3, 'cancelled')
) AS v(id, new_status)
WHERE o.id = v.id;Avoiding SELECT *
-- BAD: Fetches all columns including large text/blob
SELECT * FROM articles WHERE published = true;
-- GOOD: Only fetch needed columns
SELECT id, title, summary, author_id, published_at
FROM articles
WHERE published = true;Using EXISTS vs IN
-- For checking existence, EXISTS is often faster
-- BAD
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE total > 1000);
-- GOOD (for large subquery results)
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id AND o.total > 1000
);Materialized Views for Complex Aggregations
-- Create materialized view for expensive aggregations
CREATE MATERIALIZED VIEW daily_sales_summary AS
SELECT
date_trunc('day', created_at) as date,
product_id,
COUNT(*) as order_count,
SUM(quantity) as total_quantity,
SUM(total) as total_revenue
FROM orders
GROUP BY date_trunc('day', created_at), product_id;
-- Create index on materialized view
CREATE INDEX idx_daily_sales_date ON daily_sales_summary(date);
-- Refresh periodically
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_sales_summary;---
6. Database Migrations
Migration Best Practices
-- Always include rollback
-- migrations/20240115_001_add_user_status.sql
-- UP
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active';
CREATE INDEX CONCURRENTLY idx_users_status ON users(status);
-- DOWN (in separate file or comment)
DROP INDEX CONCURRENTLY IF EXISTS idx_users_status;
ALTER TABLE users DROP COLUMN IF EXISTS status;Safe Column Addition
-- SAFE: Add nullable column (no table rewrite)
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- SAFE: Add column with volatile default (PG 11+)
ALTER TABLE users ADD COLUMN created_at TIMESTAMP DEFAULT NOW();
-- UNSAFE: Add column with constant default (table rewrite before PG 11)
-- ALTER TABLE users ADD COLUMN score INTEGER DEFAULT 0;
-- SAFE alternative for constant default:
ALTER TABLE users ADD COLUMN score INTEGER;
UPDATE users SET score = 0 WHERE score IS NULL;
ALTER TABLE users ALTER COLUMN score SET DEFAULT 0;
ALTER TABLE users ALTER COLUMN score SET NOT NULL;Safe Index Creation
-- UNSAFE: Locks table
CREATE INDEX idx_orders_user ON orders(user_id);
-- SAFE: Non-blocking
CREATE INDEX CONCURRENTLY idx_orders_user ON orders(user_id);
-- Note: CONCURRENTLY cannot run in a transactionSafe Column Removal
-- Step 1: Stop writing to column (application change)
-- Step 2: Wait for all deployments
-- Step 3: Drop column
ALTER TABLE users DROP COLUMN IF EXISTS legacy_field;---
7. Monitoring and Alerting
Key Metrics to Monitor
-- Active connections
SELECT count(*) FROM pg_stat_activity WHERE state = 'active';
-- Connection by state
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;
-- Long-running queries
SELECT
pid,
now() - pg_stat_activity.query_start AS duration,
query,
state
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '5 minutes'
AND state != 'idle';
-- Table bloat
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as total_size,
pg_size_pretty(pg_relation_size(schemaname||'.'||tablename)) as table_size,
pg_size_pretty(pg_indexes_size(schemaname||'.'||tablename)) as index_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 10;pg_stat_statements for Query Analysis
-- Enable extension
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Find slowest queries
SELECT
round(total_exec_time::numeric, 2) as total_time_ms,
calls,
round(mean_exec_time::numeric, 2) as avg_time_ms,
round((100 * total_exec_time / sum(total_exec_time) over())::numeric, 2) as percentage,
query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
-- Find most frequent queries
SELECT
calls,
round(total_exec_time::numeric, 2) as total_time_ms,
round(mean_exec_time::numeric, 2) as avg_time_ms,
query
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 10;Alert Thresholds
| Metric | Warning | Critical |
|---|---|---|
| Connection usage | > 70% | > 90% |
| Query time P95 | > 500ms | > 2s |
| Replication lag | > 30s | > 5m |
| Disk usage | > 70% | > 85% |
| Cache hit ratio | < 95% | < 90% |
---
Quick Reference: PostgreSQL Commands
-- Check table sizes
SELECT pg_size_pretty(pg_total_relation_size('orders'));
-- Check index sizes
SELECT pg_size_pretty(pg_indexes_size('orders'));
-- Kill a query
SELECT pg_cancel_backend(pid); -- Graceful
SELECT pg_terminate_backend(pid); -- Force
-- Check locks
SELECT * FROM pg_locks WHERE granted = false;
-- Vacuum analyze (update statistics)
VACUUM ANALYZE orders;
-- Check autovacuum status
SELECT * FROM pg_stat_user_tables WHERE relname = 'orders';Backend Tools Reference
Read this when running the API scaffolder, database migration tool, or load tester — full usage examples, flag tables, and sample output.
Quick Start
# Generate API routes from OpenAPI spec
python scripts/api_scaffolder.py openapi.yaml --framework express --output src/routes/
# Analyze database schema and generate migrations
python scripts/database_migration_tool.py --connection postgres://localhost/mydb --analyze
# Load test an API endpoint
python scripts/api_load_tester.py https://api.example.com/users --concurrency 50 --duration 30Tools Overview
1. API Scaffolder
Generates API route handlers, middleware, and OpenAPI specifications from schema definitions.
Input: OpenAPI spec (YAML/JSON) or database schema Output: Route handlers, validation middleware, TypeScript types
Usage:
# Generate Express routes from OpenAPI spec
python scripts/api_scaffolder.py openapi.yaml --framework express --output src/routes/
# Output:
# Generated 12 route handlers in src/routes/
# - GET /users (listUsers)
# - POST /users (createUser)
# - GET /users/{id} (getUser)
# - PUT /users/{id} (updateUser)
# - DELETE /users/{id} (deleteUser)
# ...
# Created validation middleware: src/middleware/validators.ts
# Created TypeScript types: src/types/api.ts
# Generate from database schema
python scripts/api_scaffolder.py --from-db postgres://localhost/mydb --output src/routes/
# Generate OpenAPI spec from existing routes
python scripts/api_scaffolder.py src/routes/ --generate-spec --output openapi.yamlSupported Frameworks:
- Express.js (
--framework express) - Fastify (
--framework fastify) - Koa (
--framework koa)
2. Database Migration Tool
Analyzes database schemas, detects changes, and generates migration files with rollback support.
Input: Database connection string or schema files Output: Migration files, schema diff report, optimization suggestions
Usage:
# Analyze current schema and suggest optimizations
python scripts/database_migration_tool.py --connection postgres://localhost/mydb --analyze
# Output:
# === Database Analysis Report ===
# Tables: 24
# Total rows: 1,247,832
#
# MISSING INDEXES (5 found):
# orders.user_id - 847ms avg query time, ADD INDEX recommended
# products.category_id - 234ms avg query time, ADD INDEX recommended
#
# N+1 QUERY RISKS (3 found):
# users -> orders relationship (no eager loading)
#
# SUGGESTED MIGRATIONS:
# 1. Add index on orders(user_id)
# 2. Add index on products(category_id)
# 3. Add composite index on order_items(order_id, product_id)
# Generate migration from schema diff
python scripts/database_migration_tool.py --connection postgres://localhost/mydb \
--compare schema/v2.sql --output migrations/
# Output:
# Generated migration: migrations/20240115_add_user_indexes.sql
# Generated rollback: migrations/20240115_add_user_indexes_rollback.sql
# Dry-run a migration
python scripts/database_migration_tool.py --connection postgres://localhost/mydb \
--migrate migrations/20240115_add_user_indexes.sql --dry-run3. API Load Tester
Performs HTTP load testing with configurable concurrency, measuring latency percentiles and throughput.
Input: API endpoint URL and test configuration Output: Performance report with latency distribution, error rates, throughput metrics
Usage:
# Basic load test
python scripts/api_load_tester.py https://api.example.com/users --concurrency 50 --duration 30
# Output:
# === Load Test Results ===
# Target: https://api.example.com/users
# Duration: 30s | Concurrency: 50
#
# THROUGHPUT:
# Total requests: 15,247
# Requests/sec: 508.2
# Successful: 15,102 (99.0%)
# Failed: 145 (1.0%)
#
# LATENCY (ms):
# Min: 12
# Avg: 89
# P50: 67
# P95: 198
# P99: 423
# Max: 1,247
#
# ERRORS:
# Connection timeout: 89
# HTTP 503: 56
#
# RECOMMENDATION: P99 latency (423ms) exceeds 200ms target.
# Consider: connection pooling, query optimization, or horizontal scaling.
# Test with custom headers and body
python scripts/api_load_tester.py https://api.example.com/orders \
--method POST \
--header "Authorization: Bearer token123" \
--body '{"product_id": 1, "quantity": 2}' \
--concurrency 100 \
--duration 60
# Compare two endpoints
python scripts/api_load_tester.py https://api.example.com/v1/users https://api.example.com/v2/users \
--compare --concurrency 50 --duration 30Common Commands
# API Development
python scripts/api_scaffolder.py openapi.yaml --framework express
python scripts/api_scaffolder.py src/routes/ --generate-spec
# Database Operations
python scripts/database_migration_tool.py --connection $DATABASE_URL --analyze
python scripts/database_migration_tool.py --connection $DATABASE_URL --migrate file.sql
# Performance Testing
python scripts/api_load_tester.py https://api.example.com/endpoint --concurrency 50
python scripts/api_load_tester.py https://api.example.com/endpoint --compare baseline.jsonTool Reference
api_scaffolder.py
Purpose: Generate Express.js/Fastify/Koa route handlers, Zod validators, and TypeScript types from an OpenAPI specification.
Usage:
python scripts/api_scaffolder.py <spec> [flags]Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
spec | positional | (required) | Path to OpenAPI specification file (YAML or JSON) | |
--output | -o | string | ./generated | Output directory for generated files |
--framework | -f | choice | express | Target framework: express, fastify, or koa |
--types-only | flag | false | Generate only TypeScript type definitions, skip routes and validators | |
--verbose | -v | flag | false | Enable verbose output (shows spec title/version) |
--json | flag | false | Output results summary as JSON |
Example:
python scripts/api_scaffolder.py openapi.yaml --framework express --output src/routes/ --verboseAPI Scaffolder - Express
Spec: openapi.yaml
Output: src/routes/
--------------------------------------------------
Loaded: User Service API v1.0.0
Generated: src/routes/types.ts
Generated: src/routes/validators.ts
Generated: src/routes/users.routes.ts (5 handlers)
Generated: src/routes/index.ts
--------------------------------------------------
Generated 5 route handlers
Generated 3 type definitions
Output: src/routes/Output Formats: Human-readable console output by default. Add --json for machine-readable JSON with status, generated_files, routes_count, and types_count fields.
database_migration_tool.py
Purpose: Analyze SQL schema files for issues, compare schemas to generate migrations with rollback scripts, and suggest missing indexes.
Usage:
python scripts/database_migration_tool.py <schema> [flags]Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
schema | positional | (required) | Path to SQL schema file | |
--analyze | flag | false | Analyze schema for issues and optimizations (default mode if no other mode specified) | |
--compare | string | Path to a second schema file to compare against and generate migration | ||
--suggest-indexes | flag | false | Generate index suggestions for foreign keys, filter columns, and timestamps | |
--output | -o | string | Output directory for generated migration files | |
--verbose | -v | flag | false | Enable verbose output (shows parsed table count and info-level suggestions) |
--json | flag | false | Output results as JSON |
Example:
python scripts/database_migration_tool.py schema.sql --analyze --verboseDatabase Migration Tool
Schema: schema.sql
--------------------------------------------------
Parsed 8 tables
Analysis Results:
Tables: 8
Errors: 1
Warnings: 3
Suggestions: 7
ERRORS:
[audit_log] Table 'audit_log' has no primary key
Suggestion: Add a primary key column (e.g., 'id SERIAL PRIMARY KEY')
WARNINGS:
[orders] Foreign key column 'user_id' is not indexed
Suggestion: CREATE INDEX idx_orders_user_id ON orders(user_id);Output Formats: Human-readable console output by default. Add --json for structured JSON with issues_detail array containing severity, category, table, message, and suggestion for each finding. When using --compare --output, generates timestamped _migration.sql and _migration_rollback.sql files.
api_load_tester.py
Purpose: Perform HTTP load testing with configurable concurrency, measuring latency percentiles (p50/p90/p95/p99), throughput, error rates, and optional endpoint comparison.
Usage:
python scripts/api_load_tester.py <urls...> [flags]Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
urls | positional | (required) | One or more URLs to test | |
--method | -m | choice | GET | HTTP method: GET, POST, PUT, PATCH, or DELETE |
--body | -b | string | Request body as a JSON string | |
--header | -H | string (repeatable) | HTTP header in "Name: Value" format; can be specified multiple times | |
--concurrency | -c | int | 10 | Number of concurrent request threads |
--duration | -d | float | 10.0 | Test duration in seconds |
--timeout | -t | float | 30.0 | Per-request timeout in seconds |
--compare | flag | false | Compare two endpoints side-by-side (requires two URLs) | |
--no-verify-ssl | flag | false | Disable SSL certificate verification | |
--verbose | -v | flag | false | Enable verbose output (shows transfer bytes and throughput Mbps) |
--json | flag | false | Output results as JSON | |
--output | -o | string | File path to write JSON results |
Example:
python scripts/api_load_tester.py https://api.example.com/users \
--method GET \
--header "Authorization: Bearer tok_abc123" \
--concurrency 50 \
--duration 30 \
--verbose============================================================
LOAD TEST RESULTS
============================================================
Target: https://api.example.com/users
Method: GET
Duration: 30.2s
Concurrency: 50
THROUGHPUT:
Total requests: 14,832
Requests/sec: 491.1
Successful: 14,710 (99.2%)
Failed: 122
LATENCY (ms):
Min: 11.3
Avg: 92.4
P50: 71.2
P90: 165.8
P95: 201.3
P99: 387.6
Max: 1,102.5
StdDev: 89.2
TRANSFER:
Total bytes: 45,291,520
Throughput: 12.01 Mbps
RECOMMENDATIONS:
Warning: P99 latency (388ms) exceeds 500ms
Consider: Connection pooling, query optimization, caching
Performance looks good for this load level
============================================================Output Formats: Human-readable console report by default with latency distribution and recommendations. Add --json for structured JSON output. Use --output results.json to write results to a file. When using --compare with two URLs, outputs a side-by-side metric comparison table.
Backend Workflows & Patterns
Read this when designing an API, optimizing a database, hardening security, or looking up common response/index patterns, troubleshooting, and the success bar.
Backend Development Workflows
API Design Workflow
Use when designing a new API or refactoring existing endpoints.
Step 1: Define resources and operations
# openapi.yaml
openapi: 3.0.3
info:
title: User Service API
version: 1.0.0
paths:
/users:
get:
summary: List users
parameters:
- name: limit
in: query
schema:
type: integer
default: 20
post:
summary: Create user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUser'Step 2: Generate route scaffolding
python scripts/api_scaffolder.py openapi.yaml --framework express --output src/routes/Step 3: Implement business logic
// src/routes/users.ts (generated, then customized)
export const createUser = async (req: Request, res: Response) => {
const { email, name } = req.body;
// Add business logic
const user = await userService.create({ email, name });
res.status(201).json(user);
};Step 4: Add validation middleware
# Validation is auto-generated from OpenAPI schema
# src/middleware/validators.ts includes:
# - Request body validation
# - Query parameter validation
# - Path parameter validationStep 5: Generate updated OpenAPI spec
python scripts/api_scaffolder.py src/routes/ --generate-spec --output openapi.yamlDatabase Optimization Workflow
Use when queries are slow or database performance needs improvement.
Step 1: Analyze current performance
python scripts/database_migration_tool.py --connection $DATABASE_URL --analyzeStep 2: Identify slow queries
-- Check query execution plans
EXPLAIN ANALYZE SELECT * FROM orders
WHERE user_id = 123
ORDER BY created_at DESC
LIMIT 10;
-- Look for: Seq Scan (bad), Index Scan (good)Step 3: Generate index migrations
python scripts/database_migration_tool.py --connection $DATABASE_URL \
--suggest-indexes --output migrations/Step 4: Test migration (dry-run)
python scripts/database_migration_tool.py --connection $DATABASE_URL \
--migrate migrations/add_indexes.sql --dry-runStep 5: Apply and verify
# Apply migration
python scripts/database_migration_tool.py --connection $DATABASE_URL \
--migrate migrations/add_indexes.sql
# Verify improvement
python scripts/database_migration_tool.py --connection $DATABASE_URL --analyzeSecurity Hardening Workflow
Use when preparing an API for production or after a security review.
Step 1: Review authentication setup
// Verify JWT configuration
const jwtConfig = {
secret: process.env.JWT_SECRET, // Must be from env, never hardcoded
expiresIn: '1h', // Short-lived tokens
algorithm: 'RS256' // Prefer asymmetric
};Step 2: Add rate limiting
import rateLimit from 'express-rate-limit';
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', apiLimiter);Step 3: Validate all inputs
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100),
age: z.number().int().positive().optional()
});
// Use in route handler
const data = CreateUserSchema.parse(req.body);Step 4: Load test with attack patterns
# Test rate limiting
python scripts/api_load_tester.py https://api.example.com/login \
--concurrency 200 --duration 10 --expect-rate-limit
# Test input validation
python scripts/api_load_tester.py https://api.example.com/users \
--method POST \
--body '{"email": "not-an-email"}' \
--expect-status 400Step 5: Review security headers
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: true,
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: true,
crossOriginResourcePolicy: true,
hsts: { maxAge: 31536000, includeSubDomains: true },
}));Common Patterns Quick Reference
REST API Response Format
{
"data": { "id": 1, "name": "John" },
"meta": { "requestId": "abc-123" }
}Error Response Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid email format",
"details": [{ "field": "email", "message": "must be valid email" }]
},
"meta": { "requestId": "abc-123" }
}HTTP Status Codes
| Code | Use Case |
|---|---|
| 200 | Success (GET, PUT, PATCH) |
| 201 | Created (POST) |
| 204 | No Content (DELETE) |
| 400 | Validation error |
| 401 | Authentication required |
| 403 | Permission denied |
| 404 | Resource not found |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
Database Index Strategy
-- Single column (equality lookups)
CREATE INDEX idx_users_email ON users(email);
-- Composite (multi-column queries)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- Partial (filtered queries)
CREATE INDEX idx_orders_active ON orders(created_at) WHERE status = 'active';
-- Covering (avoid table lookup)
CREATE INDEX idx_users_email_name ON users(email) INCLUDE (name);Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
api_scaffolder.py generates empty route files | OpenAPI spec missing operationId fields or paths use unsupported HTTP methods | Add operationId to each operation; verify methods are GET, POST, PUT, PATCH, or DELETE |
database_migration_tool.py parses zero tables | SQL file uses non-standard DDL syntax or multi-line comments break regex parsing | Ensure CREATE TABLE statements end with ; and remove block comments (/* ... */) before analysis |
| Load tester reports 100% failure rate | Target URL unreachable, SSL verification failing, or firewall blocking concurrent connections | Verify URL manually with curl; try --no-verify-ssl for self-signed certs; reduce --concurrency |
Generated TypeScript types show unknown for all fields | OpenAPI schema uses $ref references to external files or missing components/schemas section | Inline referenced schemas or ensure all $ref targets exist within the same spec file |
| Migration diff reports "No changes" when changes exist | Column type differences are case-sensitive; VARCHAR(255) vs varchar(255) treated as different | Normalize casing in schema files; the parser converts types to uppercase internally |
| Load tester hangs after duration expires | Worker threads blocked on slow connections that exceed the default 30s timeout | Set --timeout lower than --duration (e.g., --timeout 5 --duration 30) to prevent thread starvation |
| Zod validators missing for nested objects | Deeply nested $ref chains not fully resolved by the scaffolder | Flatten nested schemas in the OpenAPI spec or manually extend the generated validators |
Success Criteria
- API p99 latency under 200ms at production concurrency levels, verified by
api_load_tester.py - Zero N+1 query patterns detected in schema analysis via
database_migration_tool.py --analyze - All foreign key columns indexed with no "missing index" warnings from the migration tool
- 100% of generated routes include input validation middleware (Zod schemas auto-generated from OpenAPI spec)
- Success rate above 99.5% during sustained load tests at target concurrency for 60+ seconds
- Every migration paired with a rollback script to enable zero-downtime deployment reversals
- API response format consistency across all endpoints following the standardized
data/error/metaenvelope pattern
#!/usr/bin/env python3
"""
API Load Tester
Performs HTTP load testing with configurable concurrency, measuring latency
percentiles, throughput, and error rates.
Usage:
python api_load_tester.py https://api.example.com/users --concurrency 50 --duration 30
python api_load_tester.py https://api.example.com/orders --method POST --body '{"item": 1}'
python api_load_tester.py https://api.example.com/v1/users https://api.example.com/v2/users --compare
"""
import os
import sys
import json
import argparse
import time
import statistics
import threading
import queue
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field, asdict
from typing import Dict, List, Optional, Tuple
from datetime import datetime
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
from urllib.parse import urlparse
import ssl
@dataclass
class RequestResult:
"""Result of a single HTTP request."""
success: bool
status_code: int
latency_ms: float
error: Optional[str] = None
response_size: int = 0
@dataclass
class LoadTestResults:
"""Aggregated load test results."""
target_url: str
method: str
duration_seconds: float
concurrency: int
total_requests: int
successful_requests: int
failed_requests: int
requests_per_second: float
# Latency metrics (milliseconds)
latency_min: float
latency_max: float
latency_avg: float
latency_p50: float
latency_p90: float
latency_p95: float
latency_p99: float
latency_stddev: float
# Error breakdown
errors_by_type: Dict[str, int] = field(default_factory=dict)
# Transfer metrics
total_bytes_received: int = 0
throughput_mbps: float = 0.0
def success_rate(self) -> float:
"""Calculate success rate percentage."""
if self.total_requests == 0:
return 0.0
return (self.successful_requests / self.total_requests) * 100
def calculate_percentile(data: List[float], percentile: float) -> float:
"""Calculate percentile from sorted data."""
if not data:
return 0.0
k = (len(data) - 1) * (percentile / 100)
f = int(k)
c = f + 1 if f + 1 < len(data) else f
return data[f] + (data[c] - data[f]) * (k - f)
class HTTPClient:
"""HTTP client with configurable settings."""
def __init__(self, timeout: float = 30.0, headers: Optional[Dict[str, str]] = None,
verify_ssl: bool = True):
self.timeout = timeout
self.headers = headers or {}
self.verify_ssl = verify_ssl
# Create SSL context
if not verify_ssl:
self.ssl_context = ssl.create_default_context()
self.ssl_context.check_hostname = False
self.ssl_context.verify_mode = ssl.CERT_NONE
else:
self.ssl_context = None
def request(self, url: str, method: str = 'GET', body: Optional[bytes] = None) -> RequestResult:
"""Execute HTTP request and return result."""
start_time = time.perf_counter()
try:
request = Request(url, data=body, method=method)
# Add headers
for key, value in self.headers.items():
request.add_header(key, value)
# Add content-type for POST/PUT
if body and method in ['POST', 'PUT', 'PATCH']:
if 'Content-Type' not in self.headers:
request.add_header('Content-Type', 'application/json')
# Execute request
with urlopen(request, timeout=self.timeout, context=self.ssl_context) as response:
response_data = response.read()
elapsed = (time.perf_counter() - start_time) * 1000
return RequestResult(
success=True,
status_code=response.status,
latency_ms=elapsed,
response_size=len(response_data),
)
except HTTPError as e:
elapsed = (time.perf_counter() - start_time) * 1000
return RequestResult(
success=False,
status_code=e.code,
latency_ms=elapsed,
error=f"HTTP {e.code}: {e.reason}",
)
except URLError as e:
elapsed = (time.perf_counter() - start_time) * 1000
return RequestResult(
success=False,
status_code=0,
latency_ms=elapsed,
error=f"Connection error: {str(e.reason)}",
)
except TimeoutError:
elapsed = (time.perf_counter() - start_time) * 1000
return RequestResult(
success=False,
status_code=0,
latency_ms=elapsed,
error="Connection timeout",
)
except Exception as e:
elapsed = (time.perf_counter() - start_time) * 1000
return RequestResult(
success=False,
status_code=0,
latency_ms=elapsed,
error=str(e),
)
class LoadTester:
"""HTTP load testing engine."""
def __init__(self, url: str, method: str = 'GET', body: Optional[str] = None,
headers: Optional[Dict[str, str]] = None, concurrency: int = 10,
duration: float = 10.0, timeout: float = 30.0, verify_ssl: bool = True):
self.url = url
self.method = method.upper()
self.body = body.encode() if body else None
self.headers = headers or {}
self.concurrency = concurrency
self.duration = duration
self.timeout = timeout
self.verify_ssl = verify_ssl
self.results: List[RequestResult] = []
self.stop_event = threading.Event()
self.results_lock = threading.Lock()
def run(self) -> LoadTestResults:
"""Execute load test and return results."""
print(f"Load Testing: {self.url}")
print(f"Method: {self.method}")
print(f"Concurrency: {self.concurrency}")
print(f"Duration: {self.duration}s")
print("-" * 50)
self.results = []
self.stop_event.clear()
start_time = time.time()
# Start worker threads
with ThreadPoolExecutor(max_workers=self.concurrency) as executor:
futures = []
for _ in range(self.concurrency):
future = executor.submit(self._worker)
futures.append(future)
# Wait for duration
time.sleep(self.duration)
self.stop_event.set()
# Wait for workers to finish
for future in as_completed(futures):
try:
future.result()
except Exception as e:
print(f"Worker error: {e}")
elapsed_time = time.time() - start_time
return self._aggregate_results(elapsed_time)
def _worker(self):
"""Worker thread that continuously sends requests."""
client = HTTPClient(
timeout=self.timeout,
headers=self.headers,
verify_ssl=self.verify_ssl,
)
while not self.stop_event.is_set():
result = client.request(self.url, self.method, self.body)
with self.results_lock:
self.results.append(result)
def _aggregate_results(self, elapsed_time: float) -> LoadTestResults:
"""Aggregate individual results into summary."""
if not self.results:
return LoadTestResults(
target_url=self.url,
method=self.method,
duration_seconds=elapsed_time,
concurrency=self.concurrency,
total_requests=0,
successful_requests=0,
failed_requests=0,
requests_per_second=0,
latency_min=0,
latency_max=0,
latency_avg=0,
latency_p50=0,
latency_p90=0,
latency_p95=0,
latency_p99=0,
latency_stddev=0,
)
# Separate successful and failed
successful = [r for r in self.results if r.success]
failed = [r for r in self.results if not r.success]
# Latency calculations (from successful requests)
latencies = sorted([r.latency_ms for r in successful]) if successful else [0]
# Error breakdown
errors_by_type: Dict[str, int] = {}
for r in failed:
error_type = r.error or 'Unknown'
errors_by_type[error_type] = errors_by_type.get(error_type, 0) + 1
# Calculate throughput
total_bytes = sum(r.response_size for r in successful)
throughput_mbps = (total_bytes * 8) / (elapsed_time * 1_000_000) if elapsed_time > 0 else 0
return LoadTestResults(
target_url=self.url,
method=self.method,
duration_seconds=elapsed_time,
concurrency=self.concurrency,
total_requests=len(self.results),
successful_requests=len(successful),
failed_requests=len(failed),
requests_per_second=len(self.results) / elapsed_time if elapsed_time > 0 else 0,
latency_min=min(latencies),
latency_max=max(latencies),
latency_avg=statistics.mean(latencies) if latencies else 0,
latency_p50=calculate_percentile(latencies, 50),
latency_p90=calculate_percentile(latencies, 90),
latency_p95=calculate_percentile(latencies, 95),
latency_p99=calculate_percentile(latencies, 99),
latency_stddev=statistics.stdev(latencies) if len(latencies) > 1 else 0,
errors_by_type=errors_by_type,
total_bytes_received=total_bytes,
throughput_mbps=throughput_mbps,
)
def print_results(results: LoadTestResults, verbose: bool = False):
"""Print formatted load test results."""
print("\n" + "=" * 60)
print("LOAD TEST RESULTS")
print("=" * 60)
print(f"\nTarget: {results.target_url}")
print(f"Method: {results.method}")
print(f"Duration: {results.duration_seconds:.1f}s")
print(f"Concurrency: {results.concurrency}")
print(f"\nTHROUGHPUT:")
print(f" Total requests: {results.total_requests:,}")
print(f" Requests/sec: {results.requests_per_second:.1f}")
print(f" Successful: {results.successful_requests:,} ({results.success_rate():.1f}%)")
print(f" Failed: {results.failed_requests:,}")
print(f"\nLATENCY (ms):")
print(f" Min: {results.latency_min:.1f}")
print(f" Avg: {results.latency_avg:.1f}")
print(f" P50: {results.latency_p50:.1f}")
print(f" P90: {results.latency_p90:.1f}")
print(f" P95: {results.latency_p95:.1f}")
print(f" P99: {results.latency_p99:.1f}")
print(f" Max: {results.latency_max:.1f}")
print(f" StdDev: {results.latency_stddev:.1f}")
if results.errors_by_type:
print(f"\nERRORS:")
for error_type, count in sorted(results.errors_by_type.items(), key=lambda x: -x[1]):
print(f" {error_type}: {count}")
if verbose:
print(f"\nTRANSFER:")
print(f" Total bytes: {results.total_bytes_received:,}")
print(f" Throughput: {results.throughput_mbps:.2f} Mbps")
# Recommendations
print(f"\nRECOMMENDATIONS:")
if results.latency_p99 > 500:
print(f" Warning: P99 latency ({results.latency_p99:.0f}ms) exceeds 500ms")
print(f" Consider: Connection pooling, query optimization, caching")
if results.latency_p95 > 200:
print(f" Warning: P95 latency ({results.latency_p95:.0f}ms) exceeds 200ms target")
if results.success_rate() < 99.0:
print(f" Warning: Success rate ({results.success_rate():.1f}%) below 99%")
print(f" Check server capacity and error logs")
if results.latency_stddev > results.latency_avg:
print(f" Warning: High latency variance (stddev > avg)")
print(f" Indicates inconsistent performance")
if results.success_rate() >= 99.0 and results.latency_p95 <= 200:
print(f" Performance looks good for this load level")
print("=" * 60)
def compare_results(results1: LoadTestResults, results2: LoadTestResults):
"""Compare two load test results."""
print("\n" + "=" * 60)
print("COMPARISON RESULTS")
print("=" * 60)
print(f"\n{'Metric':<25} {'Endpoint 1':<15} {'Endpoint 2':<15} {'Diff':<15}")
print("-" * 70)
# Helper to format diff
def diff_str(v1: float, v2: float, lower_better: bool = True) -> str:
if v1 == 0:
return "N/A"
diff_pct = ((v2 - v1) / v1) * 100
symbol = "-" if (diff_pct < 0) == lower_better else "+"
color_good = diff_pct < 0 if lower_better else diff_pct > 0
return f"{symbol}{abs(diff_pct):.1f}%"
metrics = [
("Requests/sec", results1.requests_per_second, results2.requests_per_second, False),
("Success rate (%)", results1.success_rate(), results2.success_rate(), False),
("Latency Avg (ms)", results1.latency_avg, results2.latency_avg, True),
("Latency P50 (ms)", results1.latency_p50, results2.latency_p50, True),
("Latency P90 (ms)", results1.latency_p90, results2.latency_p90, True),
("Latency P95 (ms)", results1.latency_p95, results2.latency_p95, True),
("Latency P99 (ms)", results1.latency_p99, results2.latency_p99, True),
]
for name, v1, v2, lower_better in metrics:
print(f"{name:<25} {v1:<15.1f} {v2:<15.1f} {diff_str(v1, v2, lower_better):<15}")
print("-" * 70)
# Summary
print(f"\nEndpoint 1: {results1.target_url}")
print(f"Endpoint 2: {results2.target_url}")
# Determine winner
score1, score2 = 0, 0
if results1.requests_per_second > results2.requests_per_second:
score1 += 1
else:
score2 += 1
if results1.latency_p95 < results2.latency_p95:
score1 += 1
else:
score2 += 1
if results1.success_rate() > results2.success_rate():
score1 += 1
else:
score2 += 1
print(f"\nOverall: {'Endpoint 1' if score1 > score2 else 'Endpoint 2'} performs better")
print("=" * 60)
class APILoadTester:
"""Main load tester class with CLI integration."""
def __init__(self, urls: List[str], method: str = 'GET', body: Optional[str] = None,
headers: Optional[Dict[str, str]] = None, concurrency: int = 10,
duration: float = 10.0, timeout: float = 30.0, compare: bool = False,
verbose: bool = False, verify_ssl: bool = True):
self.urls = urls
self.method = method
self.body = body
self.headers = headers or {}
self.concurrency = concurrency
self.duration = duration
self.timeout = timeout
self.compare = compare
self.verbose = verbose
self.verify_ssl = verify_ssl
def run(self) -> Dict:
"""Execute load test(s) and return results."""
results = []
for url in self.urls:
tester = LoadTester(
url=url,
method=self.method,
body=self.body,
headers=self.headers,
concurrency=self.concurrency,
duration=self.duration,
timeout=self.timeout,
verify_ssl=self.verify_ssl,
)
result = tester.run()
results.append(result)
if not self.compare:
print_results(result, self.verbose)
if self.compare and len(results) >= 2:
compare_results(results[0], results[1])
return {
'status': 'success',
'results': [asdict(r) for r in results],
}
def parse_headers(header_args: Optional[List[str]]) -> Dict[str, str]:
"""Parse header arguments into dictionary."""
headers = {}
if header_args:
for h in header_args:
if ':' in h:
key, value = h.split(':', 1)
headers[key.strip()] = value.strip()
return headers
def main():
"""CLI entry point."""
parser = argparse.ArgumentParser(
description='HTTP load testing tool',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
%(prog)s https://api.example.com/users --concurrency 50 --duration 30
%(prog)s https://api.example.com/orders --method POST --body '{"item": 1}'
%(prog)s https://api.example.com/v1 https://api.example.com/v2 --compare
%(prog)s https://api.example.com/health --header "Authorization: Bearer token"
'''
)
parser.add_argument(
'urls',
nargs='+',
help='URL(s) to test'
)
parser.add_argument(
'--method', '-m',
default='GET',
choices=['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
help='HTTP method (default: GET)'
)
parser.add_argument(
'--body', '-b',
help='Request body (JSON string)'
)
parser.add_argument(
'--header', '-H',
action='append',
dest='headers',
help='HTTP header (format: "Name: Value")'
)
parser.add_argument(
'--concurrency', '-c',
type=int,
default=10,
help='Number of concurrent requests (default: 10)'
)
parser.add_argument(
'--duration', '-d',
type=float,
default=10.0,
help='Test duration in seconds (default: 10)'
)
parser.add_argument(
'--timeout', '-t',
type=float,
default=30.0,
help='Request timeout in seconds (default: 30)'
)
parser.add_argument(
'--compare',
action='store_true',
help='Compare two endpoints (requires two URLs)'
)
parser.add_argument(
'--no-verify-ssl',
action='store_true',
help='Disable SSL certificate verification'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose output'
)
parser.add_argument(
'--json',
action='store_true',
help='Output results as JSON'
)
parser.add_argument(
'--output', '-o',
help='Output file path for results'
)
args = parser.parse_args()
# Validate
if args.compare and len(args.urls) < 2:
print("Error: --compare requires two URLs", file=sys.stderr)
sys.exit(1)
# Parse headers
headers = parse_headers(args.headers)
try:
tester = APILoadTester(
urls=args.urls,
method=args.method,
body=args.body,
headers=headers,
concurrency=args.concurrency,
duration=args.duration,
timeout=args.timeout,
compare=args.compare,
verbose=args.verbose,
verify_ssl=not args.no_verify_ssl,
)
results = tester.run()
if args.json:
output = json.dumps(results, indent=2)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"\nResults written to: {args.output}")
else:
print(output)
elif args.output:
with open(args.output, 'w') as f:
json.dump(results, f, indent=2)
print(f"\nResults written to: {args.output}")
except KeyboardInterrupt:
print("\nTest interrupted by user")
sys.exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
API Scaffolder
Generates Express.js route handlers, validation middleware, and TypeScript types
from OpenAPI specifications (YAML/JSON).
Usage:
python api_scaffolder.py openapi.yaml --output src/routes/
python api_scaffolder.py openapi.json --framework fastify --output src/
python api_scaffolder.py spec.yaml --types-only --output src/types/
"""
import os
import sys
import json
import argparse
import re
from pathlib import Path
from typing import Dict, List, Optional, Any
from datetime import datetime
def load_yaml_as_json(content: str) -> Dict:
"""Parse YAML content without PyYAML dependency (basic subset)."""
lines = content.split('\n')
result = {}
stack = [(result, -1)]
current_key = None
in_array = False
array_indent = -1
for line in lines:
stripped = line.lstrip()
if not stripped or stripped.startswith('#'):
continue
indent = len(line) - len(stripped)
# Pop stack until we find the right level
while len(stack) > 1 and stack[-1][1] >= indent:
stack.pop()
current_obj = stack[-1][0]
if stripped.startswith('- '):
# Array item
value = stripped[2:].strip()
if isinstance(current_obj, list):
if ':' in value:
# Object in array
key, val = value.split(':', 1)
new_obj = {key.strip(): val.strip().strip('"').strip("'")}
current_obj.append(new_obj)
stack.append((new_obj, indent))
else:
current_obj.append(value.strip('"').strip("'"))
elif ':' in stripped:
key, value = stripped.split(':', 1)
key = key.strip()
value = value.strip()
if value == '':
# Check next line for array or object
new_obj = {}
current_obj[key] = new_obj
stack.append((new_obj, indent))
elif value.startswith('[') and value.endswith(']'):
# Inline array
items = value[1:-1].split(',')
current_obj[key] = [i.strip().strip('"').strip("'") for i in items if i.strip()]
else:
# Simple value
value = value.strip('"').strip("'")
if value.lower() == 'true':
value = True
elif value.lower() == 'false':
value = False
elif value.isdigit():
value = int(value)
current_obj[key] = value
return result
def load_spec(spec_path: Path) -> Dict:
"""Load OpenAPI spec from YAML or JSON file."""
content = spec_path.read_text()
if spec_path.suffix in ['.yaml', '.yml']:
try:
import yaml
return yaml.safe_load(content)
except ImportError:
# Fallback to basic YAML parser
return load_yaml_as_json(content)
else:
return json.loads(content)
def openapi_type_to_ts(schema: Dict) -> str:
"""Convert OpenAPI schema type to TypeScript type."""
if not schema:
return 'unknown'
if '$ref' in schema:
ref = schema['$ref']
return ref.split('/')[-1]
type_map = {
'string': 'string',
'integer': 'number',
'number': 'number',
'boolean': 'boolean',
'object': 'Record<string, unknown>',
'array': 'unknown[]',
}
schema_type = schema.get('type', 'unknown')
if schema_type == 'array':
items = schema.get('items', {})
item_type = openapi_type_to_ts(items)
return f'{item_type}[]'
if schema_type == 'object':
properties = schema.get('properties', {})
if properties:
props = []
required = schema.get('required', [])
for name, prop in properties.items():
ts_type = openapi_type_to_ts(prop)
optional = '?' if name not in required else ''
props.append(f' {name}{optional}: {ts_type};')
return '{\n' + '\n'.join(props) + '\n}'
return 'Record<string, unknown>'
if 'enum' in schema:
values = ' | '.join(f"'{v}'" for v in schema['enum'])
return values
return type_map.get(schema_type, 'unknown')
def generate_zod_schema(schema: Dict, name: str) -> str:
"""Generate Zod validation schema from OpenAPI schema."""
if not schema:
return f'export const {name}Schema = z.unknown();'
def schema_to_zod(s: Dict) -> str:
if '$ref' in s:
ref_name = s['$ref'].split('/')[-1]
return f'{ref_name}Schema'
s_type = s.get('type', 'unknown')
if s_type == 'string':
zod = 'z.string()'
if 'minLength' in s:
zod += f'.min({s["minLength"]})'
if 'maxLength' in s:
zod += f'.max({s["maxLength"]})'
if 'pattern' in s:
zod += f'.regex(/{s["pattern"]}/)'
if s.get('format') == 'email':
zod += '.email()'
if s.get('format') == 'uuid':
zod += '.uuid()'
if 'enum' in s:
values = ', '.join(f"'{v}'" for v in s['enum'])
return f'z.enum([{values}])'
return zod
if s_type == 'integer':
zod = 'z.number().int()'
if 'minimum' in s:
zod += f'.min({s["minimum"]})'
if 'maximum' in s:
zod += f'.max({s["maximum"]})'
return zod
if s_type == 'number':
zod = 'z.number()'
if 'minimum' in s:
zod += f'.min({s["minimum"]})'
if 'maximum' in s:
zod += f'.max({s["maximum"]})'
return zod
if s_type == 'boolean':
return 'z.boolean()'
if s_type == 'array':
items_zod = schema_to_zod(s.get('items', {}))
return f'z.array({items_zod})'
if s_type == 'object':
properties = s.get('properties', {})
required = s.get('required', [])
if not properties:
return 'z.record(z.unknown())'
props = []
for prop_name, prop_schema in properties.items():
prop_zod = schema_to_zod(prop_schema)
if prop_name not in required:
prop_zod += '.optional()'
props.append(f' {prop_name}: {prop_zod},')
return 'z.object({\n' + '\n'.join(props) + '\n})'
return 'z.unknown()'
return f'export const {name}Schema = {schema_to_zod(schema)};'
def to_camel_case(s: str) -> str:
"""Convert string to camelCase."""
s = re.sub(r'[^a-zA-Z0-9]', ' ', s)
words = s.split()
if not words:
return s
return words[0].lower() + ''.join(w.capitalize() for w in words[1:])
def to_pascal_case(s: str) -> str:
"""Convert string to PascalCase."""
s = re.sub(r'[^a-zA-Z0-9]', ' ', s)
return ''.join(w.capitalize() for w in s.split())
def extract_path_params(path: str) -> List[str]:
"""Extract path parameters from OpenAPI path."""
return re.findall(r'\{(\w+)\}', path)
def openapi_path_to_express(path: str) -> str:
"""Convert OpenAPI path to Express path format."""
return re.sub(r'\{(\w+)\}', r':\1', path)
class APIScaffolder:
"""Generate Express.js routes from OpenAPI specification."""
SUPPORTED_FRAMEWORKS = ['express', 'fastify', 'koa']
def __init__(self, spec_path: str, output_dir: str, framework: str = 'express',
types_only: bool = False, verbose: bool = False):
self.spec_path = Path(spec_path)
self.output_dir = Path(output_dir)
self.framework = framework
self.types_only = types_only
self.verbose = verbose
self.spec: Dict = {}
self.generated_files: List[str] = []
def run(self) -> Dict:
"""Execute scaffolding process."""
print(f"API Scaffolder - {self.framework.capitalize()}")
print(f"Spec: {self.spec_path}")
print(f"Output: {self.output_dir}")
print("-" * 50)
self.validate()
self.load_spec()
self.ensure_output_dir()
if self.types_only:
self.generate_types()
else:
self.generate_types()
self.generate_validators()
self.generate_routes()
self.generate_index()
return {
'status': 'success',
'spec': str(self.spec_path),
'output': str(self.output_dir),
'framework': self.framework,
'generated_files': self.generated_files,
'routes_count': len(self.get_operations()),
'types_count': len(self.get_schemas()),
}
def validate(self):
"""Validate inputs."""
if not self.spec_path.exists():
raise FileNotFoundError(f"Spec file not found: {self.spec_path}")
if self.framework not in self.SUPPORTED_FRAMEWORKS:
raise ValueError(f"Unsupported framework: {self.framework}")
def load_spec(self):
"""Load and parse OpenAPI specification."""
self.spec = load_spec(self.spec_path)
if self.verbose:
title = self.spec.get('info', {}).get('title', 'Unknown')
version = self.spec.get('info', {}).get('version', '0.0.0')
print(f"Loaded: {title} v{version}")
def ensure_output_dir(self):
"""Create output directory if needed."""
self.output_dir.mkdir(parents=True, exist_ok=True)
def get_schemas(self) -> Dict:
"""Get component schemas from spec."""
return self.spec.get('components', {}).get('schemas', {})
def get_operations(self) -> List[Dict]:
"""Extract all operations from spec."""
operations = []
paths = self.spec.get('paths', {})
for path, methods in paths.items():
if not isinstance(methods, dict):
continue
for method, details in methods.items():
if method.lower() not in ['get', 'post', 'put', 'patch', 'delete']:
continue
if not isinstance(details, dict):
continue
op_id = details.get('operationId', f'{method}_{path}'.replace('/', '_'))
operations.append({
'path': path,
'method': method.lower(),
'operation_id': op_id,
'summary': details.get('summary', ''),
'parameters': details.get('parameters', []),
'request_body': details.get('requestBody', {}),
'responses': details.get('responses', {}),
'tags': details.get('tags', ['default']),
})
return operations
def generate_types(self):
"""Generate TypeScript type definitions."""
schemas = self.get_schemas()
lines = [
'// Auto-generated TypeScript types',
f'// Generated from: {self.spec_path.name}',
f'// Date: {datetime.now().isoformat()}',
'',
]
for name, schema in schemas.items():
ts_type = openapi_type_to_ts(schema)
if ts_type.startswith('{'):
lines.append(f'export interface {name} {ts_type}')
else:
lines.append(f'export type {name} = {ts_type};')
lines.append('')
# Generate request/response types from operations
for op in self.get_operations():
op_name = to_pascal_case(op['operation_id'])
# Request body type
req_body = op.get('request_body', {})
if req_body:
content = req_body.get('content', {})
json_content = content.get('application/json', {})
schema = json_content.get('schema', {})
if schema and '$ref' not in schema:
ts_type = openapi_type_to_ts(schema)
lines.append(f'export interface {op_name}Request {ts_type}')
lines.append('')
# Response type (200 response)
responses = op.get('responses', {})
success_resp = responses.get('200', responses.get('201', {}))
if success_resp:
content = success_resp.get('content', {})
json_content = content.get('application/json', {})
schema = json_content.get('schema', {})
if schema and '$ref' not in schema:
ts_type = openapi_type_to_ts(schema)
lines.append(f'export interface {op_name}Response {ts_type}')
lines.append('')
types_file = self.output_dir / 'types.ts'
types_file.write_text('\n'.join(lines))
self.generated_files.append(str(types_file))
print(f" Generated: {types_file}")
def generate_validators(self):
"""Generate Zod validation schemas."""
schemas = self.get_schemas()
lines = [
"import { z } from 'zod';",
'',
'// Auto-generated Zod validation schemas',
f'// Generated from: {self.spec_path.name}',
'',
]
for name, schema in schemas.items():
zod_schema = generate_zod_schema(schema, name)
lines.append(zod_schema)
lines.append(f'export type {name} = z.infer<typeof {name}Schema>;')
lines.append('')
# Generate validation middleware
lines.extend([
'// Validation middleware factory',
'import { Request, Response, NextFunction } from "express";',
'',
'export function validate<T>(schema: z.ZodSchema<T>) {',
' return (req: Request, res: Response, next: NextFunction) => {',
' const result = schema.safeParse(req.body);',
' if (!result.success) {',
' return res.status(400).json({',
' error: {',
' code: "VALIDATION_ERROR",',
' message: "Request validation failed",',
' details: result.error.errors.map(e => ({',
' field: e.path.join("."),',
' message: e.message,',
' })),',
' },',
' });',
' }',
' req.body = result.data;',
' next();',
' };',
'}',
])
validators_file = self.output_dir / 'validators.ts'
validators_file.write_text('\n'.join(lines))
self.generated_files.append(str(validators_file))
print(f" Generated: {validators_file}")
def generate_routes(self):
"""Generate route handlers."""
operations = self.get_operations()
# Group by tag
routes_by_tag: Dict[str, List[Dict]] = {}
for op in operations:
tag = op['tags'][0] if op['tags'] else 'default'
if tag not in routes_by_tag:
routes_by_tag[tag] = []
routes_by_tag[tag].append(op)
# Generate a route file per tag
for tag, ops in routes_by_tag.items():
self.generate_route_file(tag, ops)
def generate_route_file(self, tag: str, operations: List[Dict]):
"""Generate a single route file."""
tag_name = to_camel_case(tag)
lines = [
"import { Router, Request, Response, NextFunction } from 'express';",
"import { validate } from './validators';",
"import * as schemas from './validators';",
'',
f'const router = Router();',
'',
]
for op in operations:
method = op['method']
path = openapi_path_to_express(op['path'])
handler_name = to_camel_case(op['operation_id'])
summary = op.get('summary', '')
# Check if has request body
req_body = op.get('request_body', {})
has_body = bool(req_body.get('content', {}).get('application/json'))
# Find schema reference
schema_ref = None
if has_body:
content = req_body.get('content', {}).get('application/json', {})
schema = content.get('schema', {})
if '$ref' in schema:
schema_ref = schema['$ref'].split('/')[-1]
lines.append(f'/**')
if summary:
lines.append(f' * {summary}')
lines.append(f' * {method.upper()} {op["path"]}')
lines.append(f' */')
middleware = ''
if schema_ref:
middleware = f'validate(schemas.{schema_ref}Schema), '
lines.append(f"router.{method}('{path}', {middleware}async (req: Request, res: Response, next: NextFunction) => {{")
lines.append(' try {')
# Extract path params
path_params = extract_path_params(op['path'])
if path_params:
lines.append(f" const {{ {', '.join(path_params)} }} = req.params;")
lines.append('')
lines.append(f' // TODO: Implement {handler_name}')
lines.append('')
# Default response based on method
if method == 'post':
lines.append(" res.status(201).json({ message: 'Created' });")
elif method == 'delete':
lines.append(" res.status(204).send();")
else:
lines.append(" res.json({ message: 'OK' });")
lines.append(' } catch (err) {')
lines.append(' next(err);')
lines.append(' }')
lines.append('});')
lines.append('')
lines.append(f'export default router;')
route_file = self.output_dir / f'{tag_name}.routes.ts'
route_file.write_text('\n'.join(lines))
self.generated_files.append(str(route_file))
print(f" Generated: {route_file} ({len(operations)} handlers)")
def generate_index(self):
"""Generate index file that combines all routes."""
operations = self.get_operations()
# Get unique tags
tags = set()
for op in operations:
tag = op['tags'][0] if op['tags'] else 'default'
tags.add(tag)
lines = [
"import { Router } from 'express';",
'',
]
for tag in sorted(tags):
tag_name = to_camel_case(tag)
lines.append(f"import {tag_name}Routes from './{tag_name}.routes';")
lines.extend([
'',
'const router = Router();',
'',
])
for tag in sorted(tags):
tag_name = to_camel_case(tag)
# Use tag as base path
base_path = '/' + tag.lower().replace(' ', '-')
lines.append(f"router.use('{base_path}', {tag_name}Routes);")
lines.extend([
'',
'export default router;',
])
index_file = self.output_dir / 'index.ts'
index_file.write_text('\n'.join(lines))
self.generated_files.append(str(index_file))
print(f" Generated: {index_file}")
def main():
"""CLI entry point."""
parser = argparse.ArgumentParser(
description='Generate Express.js routes from OpenAPI specification',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
%(prog)s openapi.yaml --output src/routes/
%(prog)s spec.json --framework fastify --output src/api/
%(prog)s openapi.yaml --types-only --output src/types/
'''
)
parser.add_argument(
'spec',
help='Path to OpenAPI specification (YAML or JSON)'
)
parser.add_argument(
'--output', '-o',
default='./generated',
help='Output directory (default: ./generated)'
)
parser.add_argument(
'--framework', '-f',
choices=['express', 'fastify', 'koa'],
default='express',
help='Target framework (default: express)'
)
parser.add_argument(
'--types-only',
action='store_true',
help='Generate only TypeScript types'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose output'
)
parser.add_argument(
'--json',
action='store_true',
help='Output results as JSON'
)
args = parser.parse_args()
try:
scaffolder = APIScaffolder(
spec_path=args.spec,
output_dir=args.output,
framework=args.framework,
types_only=args.types_only,
verbose=args.verbose,
)
results = scaffolder.run()
print("-" * 50)
print(f"Generated {results['routes_count']} route handlers")
print(f"Generated {results['types_count']} type definitions")
print(f"Output: {results['output']}")
if args.json:
print(json.dumps(results, indent=2))
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
Related skills
FAQ
Which stacks does senior-backend support?
senior-backend targets Node.js development with Express or Fastify and PostgreSQL-backed APIs. The skill covers REST and GraphQL design, query optimization, authentication, migrations, microservices, caching, queues, and load testing at version 1.0.0.
When should senior-backend run during a project?
senior-backend fits designing new APIs, optimizing database access, implementing auth, planning migrations, load testing services, and reviewing backend pull requests. Trigger phrases include REST API design, PostgreSQL optimization, GraphQL setup, and microservices architecture.
Is Senior Backend safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.