Skip to content
Development
Agent

middleware-patterns

Error handling middleware, validation, rate limiting, and security header patterns for Node.js APIs

From plugin
vexjoy-agent
449198 skills198 agents12 commands83 hooks

Claude Code

$ npx -y skills add notque/vexjoy-agent --agent claude-code

Agent definition

middleware-patterns.md
description: Error handling middleware, validation, rate limiting, and security header patterns for Node.js APIs

Express/Next.js Middleware Patterns

> **Scope**: Express 4.x/5.x and Next.js 14+ API route middleware — error handling, Zod validation, rate limiting, CORS, and security headers. > **Version range**: Express 4.18+, Next.js 14+, `zod` 3.22+, `express-rate-limit` 7.x > **Generated**: 2026-04-08

---

Overview

Middleware order is execution-critical. Error middleware (4-arg) must come last. Rate limiting before auth avoids leaking timing data. Validation before handlers prevents untrusted data processing.

---

Pattern Table

| Pattern | Version | Use When | Avoid When | |---------|---------|----------|------------| | Error middleware `(err, req, res, next)` | Express 4+ | All error handling | 3-arg function — not called by Express for errors | | `zod.safeParse()` | zod 3+ | Collecting all field errors | `zod.parse()` — throws on first error | | `express-rate-limit` with Redis store | express-rate-limit 7+ | Multi-process / multi-instance deployments | Single-process (memory store OK) | | `helmet()` | helmet 7+ | All Express apps | Manually setting individual security headers | | Next.js `middleware.ts` | Next.js 12+ | Edge-level auth/redirect | Heavy computation — runs on every request |

---

Correct Patterns

Centralized Error Handling Middleware

import { Request, Response, NextFunction } from 'express';

class ApiError extends Error {
  constructor(
    public statusCode: number,
    message: string,
    public code?: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

// MUST have exactly 4 parameters — Express identifies error middleware by arity
function errorHandler(
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction // Required even if unused
): void {
  if (err instanceof ApiError) {
    res.status(err.statusCode).json({
      error: err.message,
      code: err.code,
      requestId: req.id, // From request ID middleware
    });
    return;
  }

  // Zod validation errors
  if (err.name === 'ZodError') {
    res.status(422).json({
      error: 'Validation failed',
      fields: JSON.parse(err.message),
    });
    return;
  }

  // Unknown errors — sanitize before sending
  console.error('[error]', err);
  res.status(500).json({
    error: 'Internal server error',
    requestId: req.id,
    // DO NOT include err.message or err.stack in production
  });
}

// Register LAST in middleware chain
app.use(errorHandler);

**Why**: Express identifies error middleware by arity. A 3-argument function is never called for errors.

---

Zod Request Validation Middleware

import { z, ZodSchema } from 'zod';
import { Request, Response, NextFunction } from 'express';

function validateBody<T>(schema: ZodSchema<T>) {
  return (req: Request, res: Response, next: NextFunction): void => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      // Collect ALL field errors, not just the first
      const errors = result.error.issues.reduce<Record<string, string[]>>(
        (acc, issue) => {
          const field = issue.path.join('.');
          acc[field] = [...(acc[field] ?? []), issue.message];
          return acc;
        },
        {}
      );
      res.status(422).json({ error: 'Validation failed', fields: errors });
      return;
    }
    // Replace req.body with validated + type-safe data
    req.body = result.data;
    next();
  };
}

// Usage:
const CreateUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8).max(128),
  name: z.string().min(1).max(100),
});

app.post('/users', validateBody(CreateUserSchema), async (req, res) => {
  // req.body is now typed as z.infer<typeof CreateUserSchema>
  const { email, password, name } = req.body;
  // ...
});

**Why**: `safeParse` collects all errors in one pass. `parse()` throws on first error only.

---

Rate Limiting with Redis Store

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';

const redis = createClient({ url: process.env.REDIS_URL });

// General API rate limit
const apiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  limit: 100,
  standardHeaders: 'draft-7', // Sends RateLimit-* headers
  legacyHeaders: false,
  store: new RedisStore({
    sendCommand: (...args: string[]) => redis.sendCommand(args),
  }),
  keyGenerator: (req) => req.user?.id ?? req.ip ?? 'anonymous',
  message: { error: 'Too many requests', retryAfter: 60 },
});

// Strict limit for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  limit: 5,
  store: new RedisStore({
    sendCommand: (...args: string[]) => redis.sendCommand(args),
  }),
  skipSuccessfulRequests: true, // Only count failed auth attempts
});

app.use('/api', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/forgot-password', authLimiter);

**Why**: In-memory store doesn't work across multiple processes. Attackers bypass by distributing requests across pods. Redis store is shared.

---

Security Headers with Helmet

import helmet from 'helmet';
import cors from 'cors';

// Helmet sets: X-Frame-Options, X-Content-Type-Options, HSTS, CSP, etc.
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'nonce-{nonce}'"], // Replace nonce per request
      imgSrc: ["'self'", 'data:', 'https:'],
      connectSrc: ["'self'", process.env.API_URL!],
    },
  },
  hsts: {
    maxAge: 31536000, // 1 year
    includeSubDomains: true,
    preload: true,
  },
}));

// CORS: explicit allowlist, not '*'
app.use(cors({
  origin: (origin, callback) => {
    const allowList = (process.env.CORS_ORIGINS ?? '').split(',');
    if (!origin || allowList.includes(origin)) {
      callback(null, true);
    } else {
      c
Read more
@notque
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. VexJoy Agent connects plain-English requests to specialist agents, skills, and workflows. /do selects the knowledge and tools needed for your task.

44964 skills198 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
449
Star history of notque/vexjoy-agent on GitHub, a chart by star-history.com

Other agents on vexjoy-agent.

modules

modules

Agent

**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes…

@notque@notqueInstall Agent
testing

testing

Agent

**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**:…

@notque@notqueInstall Agent
communication-patterns

communication-patterns

Agent

**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what…

@notque@notqueInstall Agent

More agents from other plugins.

a11y-architect
Auto-invoked

a11y-architect

Agent

Accessibility Architect specializing in WCAG 2.2 compliance for Web and Native platforms. Use PROACTIVELY when designing UI…

ecc13d293k
@affaan-m@affaan-mInstall Agent
analyze-pass-impact

analyze-pass-impact

Agent

Analyzes how a specific topic affects a group of compiler passes. Used by the /plan-update skill to parallelize research across…

react2d252k
@facebook@facebookInstall Agent
orchestrator

orchestrator

Agent

- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as…

codex2d135k
@openai@openaiInstall Agent
cavecrew-builder

cavecrew-builder

Agent

Surgical 1-2 file edit. Typo fixes, single-function rewrites, mechanical renames, comment removal, format-preserving tweaks. Hard…

caveman3d119k
@juliusbrussee@juliusbrusseeInstall Agent

Questions about middleware-patterns.

Which coding agents can use middleware-patterns?
middleware-patterns installs in Claude Code and Cursor: each loads subagents from .claude/agents, so copy the file there. Flowy's auto-invocation runs in Claude Code only.