$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Rule Writing

CLAUDE.md for Express.js and Node

Express is minimal by design — AI fills the gaps with inconsistent patterns. Rules for middleware, error handling, validation, project structure, and security.

8 min read·July 25, 2024

Express gives you freedom — AI fills it with inconsistent patterns

Middleware ordering, centralized errors, Zod validation, and security hardening

Why Express Needs AI Rules More Than Opinionated Frameworks

Express is deliberately minimal — it gives you a request, a response, and middleware. Everything else is your choice: project structure, validation library, error handling pattern, authentication approach. This flexibility is Express's strength and the reason AI generates wildly inconsistent code across routes. Without rules, every route handler is a unique snowflake with its own error handling, validation, and response format.

The most common AI failures: inline error handling (try/catch in every route instead of centralized middleware), no input validation (trusting req.body directly), inconsistent response formats ({data} vs {result} vs raw objects), no security middleware (missing helmet, cors, rate limiting), and callback-style code instead of async/await.

Express rules don't just improve consistency — they prevent security vulnerabilities. An Express app without helmet, without input validation, and without rate limiting is a target. These rules establish the security baseline that AI skips by default.

Rule 1: Middleware Ordering and Pipeline

The rule: 'Middleware is applied in order — order matters. Use this sequence: 1) helmet() for security headers, 2) cors() for CORS, 3) express.json() for body parsing, 4) rateLimit() for request throttling, 5) requestId middleware for tracing, 6) logger middleware, 7) auth middleware (on protected routes), 8) route handlers, 9) 404 handler, 10) error handler (must be last, with 4 arguments). Never put the error handler before routes — it won't catch route errors.'

For route-level middleware: 'Apply auth middleware to route groups, not individual routes: router.use(authMiddleware). Apply validation middleware per route: router.post("/users", validate(createUserSchema), createUser). Keep route handlers focused — middleware handles cross-cutting concerns.'

AI assistants scatter middleware randomly — cors after routes (doesn't work), error handler before routes (never catches), and no security middleware at all. One ordered list prevents every middleware ordering bug.

  • Order: helmet → cors → bodyParser → rateLimit → requestId → logger → auth → routes → 404 → errorHandler
  • Error handler must be last — 4 arguments: (err, req, res, next)
  • Auth middleware on route groups: router.use(auth)
  • Validation middleware per route: router.post(path, validate(schema), handler)
  • Never put error handler before routes — it won't catch anything
⚠️ Order Matters

Error handler before routes catches nothing. CORS after routes doesn't work. Middleware executes in registration order — one ordered list in your rules prevents every middleware ordering bug.

Rule 2: Centralized Error Handling

The rule: 'Use a single centralized error handler middleware. Define custom error classes: class AppError extends Error { constructor(message, statusCode, code) { super(message); this.statusCode = statusCode; this.code = code; } }. Route handlers throw errors — the error middleware catches them and sends consistent responses. Never try/catch in every route — use express-async-errors or a wrapper to forward async errors.'

For the error middleware: 'The error handler maps error types to HTTP responses: AppError → structured response with status code. ValidationError → 400 with field-level errors. AuthError → 401/403. Unknown errors → 500 with generic message (never expose internal error details to clients). Log the full error server-side with request context.'

For async errors: 'Use express-async-errors (one import, patches Express to catch async throws) or wrap route handlers: const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next). Without this, unhandled Promise rejections silently hang the request — Express doesn't catch async errors by default.'

💡 express-async-errors

Express doesn't catch async errors by default — unhandled Promise rejections silently hang requests. One import (express-async-errors) patches Express to forward async throws to your error handler. Install it first.

Rule 3: Input Validation with Zod

The rule: 'Validate all request input at the route level with Zod. Define schemas for every route's body, params, and query: const createUserSchema = z.object({ body: z.object({ email: z.string().email(), name: z.string().min(1) }), params: z.object({}), query: z.object({}) }). Create a validate middleware that parses and throws on failure. Never access req.body without validation — it's unknown by default.'

For the validate middleware: 'Parse the request against the schema: const validated = schema.parse({ body: req.body, params: req.params, query: req.query }). Attach validated data to the request: req.validated = validated. Route handlers access req.validated — never raw req.body. If parsing fails, Zod throws ZodError — the error middleware catches it and returns 400 with field-level errors.'

AI assistants skip validation entirely or use it inconsistently — some routes validate, others don't. A validate middleware that's required on every route (enforced by code review or lint rule) eliminates this class of vulnerability.

Rule 4: Project Structure

The rule: 'Use a layered structure: routes/ (HTTP handling, validation, response formatting), services/ (business logic, orchestration), repositories/ (database access), middleware/ (cross-cutting concerns), models/ (data types, schemas), utils/ (pure helpers). Routes call services, services call repositories. Never import repositories in routes — the service layer is the boundary.'

For route organization: 'Group routes by resource: routes/users.ts, routes/orders.ts, routes/auth.ts. Each route file exports a Router. The main app file mounts routers: app.use("/api/users", usersRouter). Keep route files focused — under 100 lines. Complex logic lives in the service layer.'

For configuration: 'Use a config module that reads environment variables once at startup: config.ts exports typed config values. Validate config at startup — crash fast if required env vars are missing. Never read process.env directly in route handlers or services — always go through the config module.'

  • Layers: routes → services → repositories (uni-directional dependencies)
  • Routes: HTTP handling, validation, response formatting — under 100 lines
  • Services: business logic, orchestration — no HTTP concerns
  • Repositories: database access — no business logic
  • Config module: typed env vars, validated at startup, never raw process.env

Rule 5: Security Hardening

The rule: 'Use helmet() for security headers (CSP, HSTS, X-Frame-Options). Use cors() with explicit origin whitelist — never cors() with no options (allows all origins). Use express-rate-limit on all routes (and stricter limits on auth routes). Use hpp() to prevent HTTP parameter pollution. Set secure cookie options: httpOnly: true, secure: true, sameSite: "strict".'

For authentication: 'Use bcrypt (cost factor 12+) for password hashing. Use JWT with short expiration (15 minutes) and refresh tokens for session management. Store refresh tokens in httpOnly cookies — never localStorage. Validate JWT on every request with auth middleware. Rotate signing keys periodically.'

For production: 'Disable Express's X-Powered-By header (helmet does this). Set NODE_ENV=production. Enable request logging with request IDs for tracing. Use process managers (PM2, systemd) for crash recovery. Use health check endpoints for load balancer probes.'

ℹ️ cors() Without Options = Danger

cors() with no config allows requests from any origin. AI generates this because it 'works.' In production, always whitelist specific origins: cors({ origin: ['https://app.example.com'] }).

Complete Express.js Rules Template

Consolidated rules for Express.js / Node.js projects.

  • Middleware order: helmet → cors → json → rateLimit → logger → auth → routes → 404 → error
  • Centralized error handler: custom AppError classes, 4-arg middleware, express-async-errors
  • Zod validation on every route: body, params, query — never raw req.body
  • Layered structure: routes → services → repositories — config module for env vars
  • helmet() + cors(whitelist) + rate-limit + hpp — never permissive defaults
  • bcrypt for passwords — JWT with refresh tokens — httpOnly secure cookies
  • async/await only — express-async-errors for uncaught promise handling
  • eslint + prettier — supertest for route testing — jest/vitest for unit tests
CLAUDE.md for Express.js and Node — RuleSync Blog