al boundaries. Below is the implementation strategy with new code examples and rationale.
1. Runtime Fingerprint and Version Locking
Explicitly declare the Express and Node versions. Express 4 and Express 5 differ significantly in async error propagation. Express 5 natively catches promise rejections; Express 4 does not. Locking the version prevents the AI from generating Express 5 patterns for an Express 4 codebase.
## Runtime Fingerprint
- Express: 4.19.2
- Node: 20.x LTS
- TypeScript: 5.4 (strict: true)
2. Async Flow Control Protocol
For Express 4, all async route handlers must be wrapped to catch rejections and forward them to the error middleware. Define a reusable wrapper pattern.
Rationale: This prevents silent crashes and ensures consistent error propagation.
// utils/routeGuard.ts
import { Request, Response, NextFunction } from 'express';
type AsyncRouteHandler = (req: Request, res: Response, next: NextFunction) => Promise<void>;
export const routeGuard = (handler: AsyncRouteHandler) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(handler(req, res, next)).catch(next);
};
};
AI Instruction:
## Async Safety
- All async route handlers MUST use the `routeGuard` wrapper.
- NEVER write bare async functions as route handlers.
3. Error Middleware Contract
Express identifies error handlers by arity. The signature must always be (err, req, res, next). Define a standard error barrier that handles production safety.
Rationale: Ensures errors are caught and prevents information leakage.
// middleware/errorBarrier.ts
import { Request, Response, NextFunction } from 'express';
export const errorBarrier = (
err: Error,
req: Request,
res: Response,
next: NextFunction
) => {
const statusCode = (err as any).statusCode || 500;
const responsePayload = {
error: {
message: err.message || 'Internal Server Error',
code: (err as any).code || 'UNKNOWN_ERROR',
status: statusCode,
},
};
if (process.env.NODE_ENV !== 'production') {
(responsePayload.error as any).stack = err.stack;
}
res.status(statusCode).json(responsePayload);
};
AI Instruction:
## Error Signatures
- Error middleware MUST have exactly 4 parameters: (err, req, res, next).
- Use `errorBarrier` as the global error handler.
- NEVER expose stack traces in production responses.
Mandate schema-based validation for all request inputs. Ad-hoc checks are error-prone and inconsistent.
Rationale: Centralizes validation logic and provides type safety.
// schemas/billingSchema.ts
import { z } from 'zod';
export const PaymentIntentSchema = z.object({
amount: z.number().positive(),
currency: z.string().length(3),
metadata: z.record(z.string()).optional(),
});
export type PaymentIntentInput = z.infer<typeof PaymentIntentSchema>;
AI Instruction:
## Input Contracts
- ALL route inputs MUST be validated using Zod schemas.
- Define schemas in dedicated `schemas/` files.
- Parse and validate before business logic execution.
5. Router Topology and Module Boundaries
Route files must export router instances, not mount themselves. This prevents circular dependencies and improves testability.
Rationale: Decouples route definition from application mounting.
// routes/ledgerRouter.ts
import { Router } from 'express';
import { routeGuard } from '../utils/routeGuard';
import { getTransactions } from '../controllers/ledgerController';
export const ledgerRouter = Router();
ledgerRouter.get(
'/transactions',
routeGuard(getTransactions)
);
AI Instruction:
## Module Architecture
- Route files MUST export `express.Router()` instances.
- Route files MUST NOT import the app instance or call `app.use()`.
- Mounting occurs exclusively in the main application entry point.
6. Payload Parsing Rules
Webhook endpoints require raw body streams. Applying JSON parsing globally breaks signature verification.
Rationale: Preserves raw body integrity for cryptographic verification.
// app.ts
import express from 'express';
import { ledgerRouter } from './routes/ledgerRouter';
const app = express();
// Global JSON parsing
app.use(express.json());
// Raw parsing for webhooks
app.use('/hooks/stripe', express.raw({ type: 'application/json' }));
app.use('/hooks/stripe', ledgerRouter);
AI Instruction:
## Body Parsing
- Routes requiring raw body (webhooks) MUST use `express.raw()`.
- NEVER apply `express.json()` to webhook routes.
- Configure raw parsing before route mounting.
7. Configuration Rigor
Environment variables must be validated at startup. Scattered process.env access leads to runtime failures.
Rationale: Fails fast on missing configuration and provides type safety.
// config/envManifest.ts
import { z } from 'zod';
const EnvSchema = z.object({
PORT: z.string().transform(Number),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
NODE_ENV: z.enum(['development', 'test', 'production']),
});
export const env = EnvSchema.parse(process.env);
AI Instruction:
## Configuration
- Use a validated config module (`envManifest.ts`).
- NEVER access `process.env` directly in route handlers.
- Import `env` from the config module.
Pitfall Guide
1. The Arity Blindness
Explanation: Express uses fn.length to detect error handlers. If the AI generates a handler with three arguments, Express treats it as standard middleware, and errors bypass it.
Fix: Enforce the four-argument signature (err, req, res, next) in the guardrails.
2. The Promise Trap
Explanation: In Express 4, async route handlers that throw or reject do not automatically trigger next(err). The request hangs, or the process crashes.
Fix: Mandate the routeGuard wrapper or explicit try/catch blocks for all async handlers.
3. Post-Next Mutation
Explanation: Modifying req or res after calling next() can cause "Headers already sent" errors or race conditions, as downstream middleware may have already responded.
Fix: Perform all mutations before calling next(). Use res.on('finish') for post-response logic.
4. Body Stream Consumption
Explanation: express.json() consumes the request stream. If applied before a webhook handler, the raw body is unavailable for signature verification.
Fix: Isolate raw body parsing to specific routes using express.raw() before mounting the router.
5. Circular Mounting
Explanation: Route files that import app and call app.use() create circular dependencies and make unit testing impossible.
Fix: Route files must export routers. Mounting is centralized in the entry point.
6. Stack Trace Leakage
Explanation: Including stack traces in production error responses exposes internal implementation details, aiding attackers.
Fix: Check NODE_ENV in the error handler and omit stack traces in production.
7. Global Middleware Bloat
Explanation: Applying feature-specific middleware (e.g., auth) globally via app.use() applies it to all routes, including health checks and public endpoints.
Fix: Use router-level middleware for feature isolation. Apply global middleware only for cross-cutting concerns.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Express Version | Lock to 4.19.2 or migrate to 5.x | Express 5 handles async errors natively; Express 4 requires wrappers. | Migration cost vs. Maintenance overhead. |
| Validation Library | Zod | Runtime validation with TypeScript inference; strict schema enforcement. | Low bundle size; high developer productivity. |
| Error Handling | routeGuard + errorBarrier | Centralizes error propagation; prevents silent failures. | Minimal code overhead; high reliability. |
| Body Parsing | Route-specific parsers | Prevents stream consumption conflicts; supports webhooks. | Slightly more config; prevents critical bugs. |
| Testing Strategy | Mocks for middleware; Supertest for routes | Isolates middleware logic; tests full request lifecycle for routes. | Faster test suites; better coverage. |
Configuration Template
Copy this template into your CLAUDE.md file to enforce the guardrails.
## Runtime Fingerprint
- Express: 4.19.2
- Node: 20.x LTS
- TypeScript: 5.4 (strict: true)
## Async Safety
- All async route handlers MUST use the `routeGuard` wrapper.
- NEVER write bare async functions as route handlers.
## Error Signatures
- Error middleware MUST have exactly 4 parameters: (err, req, res, next).
- Use `errorBarrier` as the global error handler.
- NEVER expose stack traces in production responses.
## Input Contracts
- ALL route inputs MUST be validated using Zod schemas.
- Define schemas in dedicated `schemas/` files.
- Parse and validate before business logic execution.
## Module Architecture
- Route files MUST export `express.Router()` instances.
- Route files MUST NOT import the app instance or call `app.use()`.
- Mounting occurs exclusively in the main application entry point.
## Body Parsing
- Routes requiring raw body (webhooks) MUST use `express.raw()`.
- NEVER apply `express.json()` to webhook routes.
- Configure raw parsing before route mounting.
## Configuration
- Use a validated config module (`envManifest.ts`).
- NEVER access `process.env` directly in route handlers.
- Import `env` from the config module.
## Security
- Use `helmet()` as the first middleware.
- Configure security headers via Helmet options; do not set manually.
Quick Start Guide
- Create
CLAUDE.md: Add the configuration template to your project root.
- Implement Core Utilities: Create
routeGuard.ts, errorBarrier.ts, and envManifest.ts based on the code examples.
- Verify Version Lock: Ensure your
package.json matches the Express version in CLAUDE.md.
- Generate Code: Use AI to scaffold routes; verify output adheres to guardrails.
- Run Tests: Execute unit tests for middleware and integration tests for routes to confirm behavior.