Claude Skill

api-framework-express

Express.js routes, middleware, error handling, request/response patterns

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-framework-express_skills_api-framework-express-3a51ef5.zip · 15 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-framework-express/skills/api-framework-express
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

API Development with Express.js

Quick Guide: Express uses middleware-based request processing. The three non-negotiable patterns: modular routing via express.Router(), centralized error handling with 4-argument middleware (err, req, res, next), and correct middleware ordering (security first, error handler last). Express 5 (now stable, default on npm) auto-forwards async errors; Express 4 requires manual next(err) or a wrapper.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST define error-handling middleware with 4 arguments: (err, req, res, next) - Express identifies error handlers by arity)

(You MUST register error handlers AFTER all routes and other middleware)

(You MUST call next(err) to forward async errors in Express 4 - Express 5 auto-forwards rejected promises)

(You MUST use express.json() and express.urlencoded() for body parsing - req.body is undefined without them)

</critical_requirements>


Auto-detection: Express.js, express, app.use, app.get, app.post, app.put, app.delete, express.Router, req.params, req.query, req.body, res.json, res.status, middleware, next(), error handler, router.use, express.static, express.json, express.urlencoded

When to use:

  • Building REST APIs with composable middleware patterns
  • Need modular route organization with express.Router()
  • Require centralized error handling across all routes
  • Building APIs that need body parsing, static files, or cookie handling
  • Creating route guards for authentication/authorization

When NOT to use:

  • Need auto-generated OpenAPI documentation from schemas
  • Building edge/serverless functions where cold start matters
  • Need strict end-to-end type safety with schema validation
  • GraphQL APIs (use a dedicated GraphQL server)

Key patterns covered:

  • Middleware chain with app.use() and next()
  • Modular routes with express.Router()
  • Error handling with 4-argument middleware
  • Async error forwarding (Express 4 vs 5)
  • Request validation middleware
  • Route parameters and query string handling
  • Route guards for authentication/authorization
  • Middleware ordering (security, CORS, rate limit, parsing, routes, errors)

Detailed Resources:




<red_flags>

RED FLAGS

High Priority:

  • Error handler has only 3 arguments - Express treats it as regular middleware, errors silently ignored
  • Error handler registered before routes - Never catches route errors
  • Missing next(error) in async handlers (Express 4) - Unhandled promise rejection, request hangs
  • Not using express.json() middleware - req.body is undefined for JSON requests
  • Magic HTTP status codes - Use named constants (HTTP_OK = 200, HTTP_NOT_FOUND = 404)

Medium Priority:

  • All routes in single file - Creates unmaintainable God file, use express.Router()
  • Not checking res.headersSent in error handler - Causes "headers already sent" crashes
  • Default exports on route modules - Violates project conventions
  • Wildcard CORS with credentials - Browsers reject origin: "*" with credentials: true
  • Missing rate limiting on public APIs - Vulnerable to abuse

Gotchas & Edge Cases:

  • next('route') vs next(error) - String 'route' skips to next route handler; anything else triggers error handler
  • req.query values are always strings - Parse numbers with parseInt(val, 10)
  • express.static without auth - Files publicly accessible unless middleware guards them
  • Router mergeParams: true - Required to access parent route params in nested routers
  • Express 5: req.body is undefined when unparsed - was {} in Express 4, may break if (!req.body) checks

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

Before implementing ANY Express route, verify these requirements are met:

All code must follow project conventions in CLAUDE.md

(You MUST define error-handling middleware with 4 arguments: (err, req, res, next) - Express identifies error handlers by arity)

(You MUST register error handlers AFTER all routes and other middleware)

(You MUST call next(err) to forward async errors in Express 4 - Express 5 auto-forwards rejected promises)

(You MUST use express.json() and express.urlencoded() for body parsing - req.body is undefined without them)

Failure to follow these rules will cause unhandled errors and broken middleware chains.

</critical_reminders>

Files (skills)
  • examples
    • core.md 6.9 KB
      # Express.js - Core Examples
      
      > Essential setup, error handling, and async patterns. See [SKILL.md](../SKILL.md) for decision guidance.
      
      **Prerequisites**: None - these are the foundational patterns.
      
      ---
      
      ## Pattern 1: Application Setup
      
      ```typescript
      // src/app.ts
      import express from "express";
      import type { Express } from "express";
      
      import { userRoutes } from "./routes/user-routes";
      import { productRoutes } from "./routes/product-routes";
      import { errorHandler } from "./middleware/error-handler";
      
      const JSON_LIMIT = "10mb";
      
      const app: Express = express();
      
      // Built-in middleware for body parsing
      app.use(express.json({ limit: JSON_LIMIT }));
      app.use(express.urlencoded({ extended: true }));
      
      // Mount route modules
      app.use("/api/users", userRoutes);
      app.use("/api/products", productRoutes);
      
      // Error handler MUST be last
      app.use(errorHandler);
      
      export { app };
      ```
      
      **Why good:** Named constants for configuration, built-in body parsers registered early, error handler registered last, modular route mounting
      
      ### Bad Example - Error Handler in Wrong Position
      
      ```typescript
      // WRONG: Error handler before routes
      const app = express();
      app.use(errorHandler); // Too early - won't catch route errors
      app.use(express.json({ limit: "10mb" })); // Magic string
      app.use("/api/users", userRoutes);
      ```
      
      **Why bad:** Error handler before routes means it never catches errors, magic strings make configuration hard to maintain
      
      ---
      
      ## Pattern 2: Error Handling Middleware
      
      Error handlers MUST have 4 arguments: `(err, req, res, next)`. Express identifies error middleware solely by the function's arity.
      
      ### Good Example - Centralized Error Handler
      
      ```typescript
      // src/middleware/error-handler.ts
      import type { Request, Response, NextFunction } from "express";
      
      const HTTP_INTERNAL_ERROR = 500;
      
      interface AppError extends Error {
        statusCode?: number;
        code?: string;
        isOperational?: boolean;
      }
      
      const errorHandler = (
        err: AppError,
        req: Request,
        res: Response,
        next: NextFunction,
      ): void => {
        // Already sent response - delegate to Express default handler
        if (res.headersSent) {
          next(err);
          return;
        }
      
        console.error({
          message: err.message,
          stack: err.stack,
          path: req.path,
          method: req.method,
        });
      
        const statusCode = err.statusCode || HTTP_INTERNAL_ERROR;
      
        // Don't expose internal error details in production
        const message =
          statusCode === HTTP_INTERNAL_ERROR && process.env.NODE_ENV === "production"
            ? "Internal server error"
            : err.message;
      
        res.status(statusCode).json({
          error: {
            message,
            code: err.code || "INTERNAL_ERROR",
            ...(process.env.NODE_ENV !== "production" && { stack: err.stack }),
          },
        });
      };
      
      export { errorHandler };
      ```
      
      **Why good:** 4 arguments (critical for Express to recognize as error handler), checks `headersSent`, hides internals in production, logs full error context
      
      ### Bad Example - Wrong Signature
      
      ```typescript
      // WRONG - Only 3 arguments, treated as regular middleware
      const errorHandler = (err, req, res) => {
        res.status(500).json({ error: err.message }); // Magic number
      };
      // Express calls this as (req, res, next) - err is actually req!
      ```
      
      **Why bad:** Express calls this as regular middleware - `err` parameter receives `req`, completely wrong behavior
      
      ---
      
      ## Pattern 3: Async Error Forwarding
      
      ### Express 5 (Default Since 2025)
      
      Express 5 auto-forwards errors from rejected promises:
      
      ```typescript
      // Express 5: async errors auto-forwarded to error handler
      router.get("/:id", async (req: Request, res: Response) => {
        const product = await getProductById(req.params.id); // If throws, error handler catches
        res.status(HTTP_OK).json({ data: product });
      });
      ```
      
      ### Express 4 (Manual Forwarding Required)
      
      ```typescript
      // Express 4: MUST forward manually
      const HTTP_OK = 200;
      
      // Option 1: try/catch with next(error)
      router.get("/:id", async (req: Request, res: Response, next: NextFunction) => {
        try {
          const product = await getProductById(req.params.id);
          res.status(HTTP_OK).json({ data: product });
        } catch (error) {
          next(error); // REQUIRED in Express 4
        }
      });
      
      // Option 2: Promise .catch(next)
      router.get("/featured", (req: Request, res: Response, next: NextFunction) => {
        getFeaturedProducts()
          .then((products) => res.status(HTTP_OK).json({ data: products }))
          .catch(next);
      });
      ```
      
      ### asyncHandler Wrapper (Express 4)
      
      Eliminates repetitive try/catch in every route handler:
      
      ```typescript
      // src/utils/async-handler.ts
      import type { Request, Response, NextFunction, RequestHandler } from "express";
      
      type AsyncHandler = (
        req: Request,
        res: Response,
        next: NextFunction,
      ) => Promise<void>;
      
      const asyncHandler = (fn: AsyncHandler): RequestHandler => {
        return (req, res, next) => {
          Promise.resolve(fn(req, res, next)).catch(next);
        };
      };
      
      export { asyncHandler };
      ```
      
      **Usage:**
      
      ```typescript
      import { asyncHandler } from "../utils/async-handler";
      
      router.get(
        "/:id",
        asyncHandler(async (req, res) => {
          const product = await getProductById(req.params.id);
          res.status(HTTP_OK).json({ data: product });
          // Errors automatically forwarded to error handler
        }),
      );
      ```
      
      **Why good:** Eliminates try/catch boilerplate, errors automatically forwarded, cleaner route definitions
      
      ### Bad Example - Missing Error Forwarding
      
      ```typescript
      // WRONG - Unhandled promise rejection in Express 4
      router.get("/:id", async (req, res) => {
        const user = await getUserById(req.params.id); // If throws, request hangs forever
        res.json({ data: user });
      });
      ```
      
      **Why bad:** In Express 4, async errors don't propagate to error handler. Request hangs until timeout.
      
      ---
      
      ## Pattern 4: Custom Error Classes
      
      Create typed errors that the centralized error handler can interpret:
      
      ```typescript
      // src/errors/app-error.ts
      const HTTP_BAD_REQUEST = 400;
      const HTTP_NOT_FOUND = 404;
      const HTTP_UNAUTHORIZED = 401;
      
      class AppError extends Error {
        readonly statusCode: number;
        readonly code: string;
        readonly isOperational: boolean;
      
        constructor(message: string, statusCode: number, code: string) {
          super(message);
          this.statusCode = statusCode;
          this.code = code;
          this.isOperational = true;
          Object.setPrototypeOf(this, AppError.prototype);
        }
      }
      
      class NotFoundError extends AppError {
        constructor(resource: string) {
          super(`${resource} not found`, HTTP_NOT_FOUND, "NOT_FOUND");
        }
      }
      
      class ValidationError extends AppError {
        constructor(message: string) {
          super(message, HTTP_BAD_REQUEST, "VALIDATION_ERROR");
        }
      }
      
      export { AppError, NotFoundError, ValidationError };
      ```
      
      **Usage in routes:**
      
      ```typescript
      router.get("/:id", async (req, res, next) => {
        try {
          const user = await getUserById(req.params.id);
          if (!user) throw new NotFoundError("User");
          res.status(HTTP_OK).json({ data: user });
        } catch (error) {
          next(error);
        }
      });
      ```
      
      **Why good:** Error handler reads `statusCode` and `code` directly, `isOperational` distinguishes expected errors from crashes, class hierarchy keeps error creation clean
      
    • middleware.md 7.4 KB
      # Express.js - Middleware Examples
      
      > Request processing, validation, auth guards, and ordering. See [core.md](core.md) for error handling and async patterns.
      
      **Prerequisites**: Understand error handler pattern and async forwarding from [core.md](core.md).
      
      ---
      
      ## Request Logging Middleware
      
      ### Good Example - Request/Response Logger
      
      ```typescript
      // src/middleware/request-logger.ts
      import type { Request, Response, NextFunction } from "express";
      
      interface RequestLogInfo {
        method: string;
        path: string;
        query: Record<string, unknown>;
        timestamp: string;
      }
      
      const requestLogger = (
        req: Request,
        res: Response,
        next: NextFunction,
      ): void => {
        const startTime = Date.now();
      
        const logInfo: RequestLogInfo = {
          method: req.method,
          path: req.path,
          query: req.query,
          timestamp: new Date().toISOString(),
        };
      
        console.log("[Request]", JSON.stringify(logInfo));
      
        // Log response when finished
        res.on("finish", () => {
          const duration = Date.now() - startTime;
          console.log(
            "[Response]",
            JSON.stringify({
              ...logInfo,
              statusCode: res.statusCode,
              durationMs: duration,
            }),
          );
        });
      
        next();
      };
      
      export { requestLogger };
      ```
      
      **Why good:** Named export, typed parameters, logs both request and response, calculates duration via `res.on("finish")`
      
      ### Bad Example - Blocking Middleware
      
      ```typescript
      // WRONG - Forgot to call next()
      const middleware = (req, res, next) => {
        console.log("Request:", req.path);
        // Missing next() - request hangs forever!
      };
      ```
      
      **Why bad:** Request hangs until timeout. Must always call `next()` or end response.
      
      ---
      
      ## Validation Middleware
      
      ### Good Example - Request Body Validation
      
      ```typescript
      // src/middleware/validators.ts
      import type { Request, Response, NextFunction } from "express";
      
      const HTTP_BAD_REQUEST = 400;
      const MIN_EMAIL_LENGTH = 5;
      const MAX_EMAIL_LENGTH = 100;
      const MIN_PASSWORD_LENGTH = 8;
      
      const validateEmail = (email: unknown): boolean => {
        if (typeof email !== "string") return false;
        if (email.length < MIN_EMAIL_LENGTH || email.length > MAX_EMAIL_LENGTH)
          return false;
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
      };
      
      const validatePassword = (password: unknown): boolean => {
        if (typeof password !== "string") return false;
        return password.length >= MIN_PASSWORD_LENGTH;
      };
      
      const validateLoginBody = (
        req: Request,
        res: Response,
        next: NextFunction,
      ): void => {
        const { email, password } = req.body;
        const errors: string[] = [];
      
        if (!validateEmail(email)) {
          errors.push(
            `Email must be ${MIN_EMAIL_LENGTH}-${MAX_EMAIL_LENGTH} characters and valid format`,
          );
        }
      
        if (!validatePassword(password)) {
          errors.push(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
        }
      
        if (errors.length > 0) {
          res.status(HTTP_BAD_REQUEST).json({
            error: {
              message: "Validation failed",
              details: errors,
            },
          });
          return;
        }
      
        next();
      };
      
      export { validateLoginBody };
      ```
      
      **Usage:**
      
      ```typescript
      router.post("/login", validateLoginBody, async (req, res, next) => {
        try {
          const result = await loginUser(req.body.email, req.body.password);
          res.json({ data: result });
        } catch (error) {
          next(error);
        }
      });
      ```
      
      **Why good:** Validation logic separated from business logic, named constants for limits, early return with error details, reusable across routes
      
      ---
      
      ## Authentication Middleware
      
      ### Good Example - Auth Guard with Role Checking
      
      ```typescript
      // src/middleware/auth-guard.ts
      import type { Request, Response, NextFunction } from "express";
      
      const HTTP_UNAUTHORIZED = 401;
      const HTTP_FORBIDDEN = 403;
      const BEARER_PREFIX = "Bearer ";
      
      interface AuthenticatedRequest extends Request {
        user?: {
          id: string;
          email: string;
          role: string;
        };
      }
      
      const requireAuth = (
        req: AuthenticatedRequest,
        res: Response,
        next: NextFunction,
      ): void => {
        const authHeader = req.headers.authorization;
      
        if (!authHeader || !authHeader.startsWith(BEARER_PREFIX)) {
          res.status(HTTP_UNAUTHORIZED).json({
            error: { message: "Missing or invalid authorization header" },
          });
          return;
        }
      
        const token = authHeader.slice(BEARER_PREFIX.length);
      
        try {
          const decoded = verifyToken(token);
          req.user = decoded;
          next();
        } catch {
          res.status(HTTP_UNAUTHORIZED).json({
            error: { message: "Invalid or expired token" },
          });
        }
      };
      
      const requireRole = (...allowedRoles: string[]) => {
        return (
          req: AuthenticatedRequest,
          res: Response,
          next: NextFunction,
        ): void => {
          if (!req.user) {
            res.status(HTTP_UNAUTHORIZED).json({
              error: { message: "Authentication required" },
            });
            return;
          }
      
          if (!allowedRoles.includes(req.user.role)) {
            res.status(HTTP_FORBIDDEN).json({
              error: { message: "Insufficient permissions" },
            });
            return;
          }
      
          next();
        };
      };
      
      export { requireAuth, requireRole };
      export type { AuthenticatedRequest };
      ```
      
      **Usage:**
      
      ```typescript
      // Protect all routes in router
      router.use(requireAuth);
      
      // Specific route requires admin
      router.delete("/:id", requireRole("admin"), async (req, res, next) => {
        // Only admins reach here
      });
      ```
      
      **Why good:** `AuthenticatedRequest` extends Request with user, `requireRole` accepts variadic roles, early returns prevent further processing
      
      ---
      
      ## Middleware Ordering
      
      ### Good Example - Correct Order
      
      ```typescript
      // src/app.ts
      import express from "express";
      import helmet from "helmet";
      import cors from "cors";
      import rateLimit from "express-rate-limit";
      
      const app = express();
      
      const RATE_LIMIT_WINDOW_MS = 900000; // 15 minutes
      const RATE_LIMIT_MAX = 100;
      const JSON_LIMIT = "10mb";
      const HTTP_NOT_FOUND = 404;
      
      // 1. Security headers FIRST
      app.use(helmet());
      
      // 2. CORS (before body parsing)
      app.use(
        cors({
          origin: process.env.ALLOWED_ORIGINS?.split(",") || [],
          credentials: true,
        }),
      );
      
      // 3. Rate limiting (before parsing to save resources)
      app.use(
        rateLimit({
          windowMs: RATE_LIMIT_WINDOW_MS,
          max: RATE_LIMIT_MAX,
        }),
      );
      
      // 4. Body parsing
      app.use(express.json({ limit: JSON_LIMIT }));
      app.use(express.urlencoded({ extended: true }));
      
      // 5. Request logging
      app.use(requestLogger);
      
      // 6. Routes
      app.use("/api/users", userRoutes);
      app.use("/api/products", productRoutes);
      
      // 7. 404 handler
      app.use((req, res) => {
        res.status(HTTP_NOT_FOUND).json({ error: { message: "Route not found" } });
      });
      
      // 8. Error handler LAST
      app.use(errorHandler);
      
      export { app };
      ```
      
      **Why good:** Security first, rate limit before expensive parsing, routes in middle, error handler absolutely last
      
      ---
      
      ## Quick Reference
      
      | Middleware Type | Signature                | Purpose               |
      | --------------- | ------------------------ | --------------------- |
      | Regular         | `(req, res, next)`       | Request processing    |
      | Error           | `(err, req, res, next)`  | Error handling        |
      | Async wrapper   | Returns `RequestHandler` | Auto error forwarding |
      
      | Middleware Order  | Reason                       |
      | ----------------- | ---------------------------- |
      | Security (helmet) | Block attacks early          |
      | CORS              | Must be before routes        |
      | Rate limit        | Before parsing to save CPU   |
      | Body parsers      | Before routes that need body |
      | Logging           | Before routes for timing     |
      | Routes            | Main logic                   |
      | 404 handler       | Catch unmatched routes       |
      | Error handler     | Catch all errors LAST        |
      
    • routing.md 10.2 KB
      # Express.js - Routing Examples
      
      > Modular routes, parameters, query strings, response helpers, and versioned APIs. See [core.md](core.md) for error handling patterns.
      
      **Prerequisites**: Understand error handling and async forwarding from [core.md](core.md).
      
      ---
      
      ## Modular Routes with Router
      
      ### Good Example - CRUD User Routes
      
      ```typescript
      // src/routes/user-routes.ts
      import { Router } from "express";
      import type { Request, Response, NextFunction } from "express";
      
      const router = Router();
      
      const HTTP_OK = 200;
      const HTTP_CREATED = 201;
      const HTTP_NOT_FOUND = 404;
      const HTTP_NO_CONTENT = 204;
      
      router.get("/", async (req: Request, res: Response, next: NextFunction) => {
        try {
          const users = await getUsersFromDatabase();
          res.status(HTTP_OK).json({ data: users });
        } catch (error) {
          next(error);
        }
      });
      
      router.get("/:id", async (req: Request, res: Response, next: NextFunction) => {
        try {
          const { id } = req.params;
          const user = await getUserById(id);
      
          if (!user) {
            res.status(HTTP_NOT_FOUND).json({ error: { message: "User not found" } });
            return;
          }
      
          res.status(HTTP_OK).json({ data: user });
        } catch (error) {
          next(error);
        }
      });
      
      router.post("/", async (req: Request, res: Response, next: NextFunction) => {
        try {
          const newUser = await createUser(req.body);
          res.status(HTTP_CREATED).json({ data: newUser });
        } catch (error) {
          next(error);
        }
      });
      
      router.put("/:id", async (req: Request, res: Response, next: NextFunction) => {
        try {
          const updatedUser = await updateUser(req.params.id, req.body);
      
          if (!updatedUser) {
            res.status(HTTP_NOT_FOUND).json({ error: { message: "User not found" } });
            return;
          }
      
          res.status(HTTP_OK).json({ data: updatedUser });
        } catch (error) {
          next(error);
        }
      });
      
      router.delete(
        "/:id",
        async (req: Request, res: Response, next: NextFunction) => {
          try {
            const deleted = await deleteUser(req.params.id);
      
            if (!deleted) {
              res
                .status(HTTP_NOT_FOUND)
                .json({ error: { message: "User not found" } });
              return;
            }
      
            res.status(HTTP_NO_CONTENT).send();
          } catch (error) {
            next(error);
          }
        },
      );
      
      export { router as userRoutes };
      ```
      
      **Mount in app:**
      
      ```typescript
      // src/app.ts
      import { userRoutes } from "./routes/user-routes";
      app.use("/api/users", userRoutes);
      ```
      
      **Why good:** Modular file per resource, named exports, explicit error forwarding, HTTP status codes as constants, 204 No Content for DELETE
      
      ---
      
      ## Nested Route Parameters
      
      ### Good Example - Parent/Child Resource Validation
      
      ```typescript
      // src/routes/comment-routes.ts
      import { Router } from "express";
      import type { Request, Response, NextFunction } from "express";
      
      const router = Router();
      const HTTP_OK = 200;
      const HTTP_NOT_FOUND = 404;
      
      // GET /api/posts/:postId/comments/:commentId
      router.get(
        "/:postId/comments/:commentId",
        async (req: Request, res: Response, next: NextFunction) => {
          try {
            const { postId, commentId } = req.params;
      
            const post = await getPostById(postId);
            if (!post) {
              res
                .status(HTTP_NOT_FOUND)
                .json({ error: { message: "Post not found" } });
              return;
            }
      
            const comment = await getCommentById(postId, commentId);
            if (!comment) {
              res
                .status(HTTP_NOT_FOUND)
                .json({ error: { message: "Comment not found" } });
              return;
            }
      
            res.status(HTTP_OK).json({ data: comment });
          } catch (error) {
            next(error);
          }
        },
      );
      
      export { router as postRoutes };
      ```
      
      **Why good:** Validates parent exists before child, clear parameter extraction from `req.params`
      
      ---
      
      ## Query String Handling
      
      ### Good Example - Search with Pagination
      
      ```typescript
      // src/routes/search-routes.ts
      import { Router } from "express";
      import type { Request, Response, NextFunction } from "express";
      
      const router = Router();
      
      const HTTP_OK = 200;
      const DEFAULT_PAGE = 1;
      const DEFAULT_LIMIT = 20;
      const MAX_LIMIT = 100;
      
      interface SearchQuery {
        q?: string;
        page?: string;
        limit?: string;
        sort?: string;
        order?: "asc" | "desc";
      }
      
      // GET /api/search?q=term&page=1&limit=20&sort=createdAt&order=desc
      router.get("/", async (req: Request, res: Response, next: NextFunction) => {
        try {
          const query = req.query as SearchQuery;
      
          const searchTerm = query.q || "";
          const page = Math.max(
            DEFAULT_PAGE,
            parseInt(query.page || String(DEFAULT_PAGE), 10),
          );
          const limit = Math.min(
            MAX_LIMIT,
            parseInt(query.limit || String(DEFAULT_LIMIT), 10),
          );
          const sort = query.sort || "createdAt";
          const order = query.order === "asc" ? "asc" : "desc";
      
          const offset = (page - 1) * limit;
      
          const [results, total] = await Promise.all([
            searchItems({ query: searchTerm, offset, limit, sort, order }),
            countSearchResults(searchTerm),
          ]);
      
          res.status(HTTP_OK).json({
            data: results,
            pagination: {
              page,
              limit,
              total,
              totalPages: Math.ceil(total / limit),
            },
          });
        } catch (error) {
          next(error);
        }
      });
      
      export { router as searchRoutes };
      ```
      
      **Why good:** Typed query interface, defaults with named constants, limit capped at `MAX_LIMIT` to prevent abuse, pagination metadata in response, parallel queries with `Promise.all`
      
      ---
      
      ## Response Helpers
      
      ### Good Example - Consistent Response Format
      
      ```typescript
      // src/utils/response.ts
      import type { Response } from "express";
      
      const HTTP_OK = 200;
      const HTTP_CREATED = 201;
      const HTTP_NO_CONTENT = 204;
      const HTTP_BAD_REQUEST = 400;
      const HTTP_NOT_FOUND = 404;
      
      interface SuccessResponse<T> {
        success: true;
        data: T;
        meta?: Record<string, unknown>;
      }
      
      interface ErrorResponse {
        success: false;
        error: {
          message: string;
          code?: string;
          details?: unknown;
        };
      }
      
      const sendSuccess = <T>(
        res: Response,
        data: T,
        statusCode: number = HTTP_OK,
        meta?: Record<string, unknown>,
      ): void => {
        const response: SuccessResponse<T> = { success: true, data };
        if (meta) {
          response.meta = meta;
        }
        res.status(statusCode).json(response);
      };
      
      const sendCreated = <T>(res: Response, data: T): void => {
        sendSuccess(res, data, HTTP_CREATED);
      };
      
      const sendNoContent = (res: Response): void => {
        res.status(HTTP_NO_CONTENT).send();
      };
      
      const sendError = (
        res: Response,
        message: string,
        statusCode: number = HTTP_BAD_REQUEST,
        code?: string,
        details?: unknown,
      ): void => {
        const response: ErrorResponse = {
          success: false,
          error: { message, code, details },
        };
        res.status(statusCode).json(response);
      };
      
      const sendNotFound = (res: Response, resource: string = "Resource"): void => {
        sendError(res, `${resource} not found`, HTTP_NOT_FOUND, "NOT_FOUND");
      };
      
      export { sendSuccess, sendCreated, sendNoContent, sendError, sendNotFound };
      ```
      
      **Usage:**
      
      ```typescript
      import { sendSuccess, sendNotFound, sendCreated } from "../utils/response";
      
      router.get("/:id", async (req, res, next) => {
        try {
          const user = await getUserById(req.params.id);
          if (!user) {
            sendNotFound(res, "User");
            return;
          }
          sendSuccess(res, user);
        } catch (error) {
          next(error);
        }
      });
      
      router.post("/", async (req, res, next) => {
        try {
          const user = await createUser(req.body);
          sendCreated(res, user);
        } catch (error) {
          next(error);
        }
      });
      ```
      
      **Why good:** Consistent `{ success, data }` / `{ success, error }` shape, typed helpers, status codes as constants, reduces boilerplate
      
      ---
      
      ## Versioned API with Sub-Routers
      
      ### Good Example - API Versioning
      
      ```typescript
      // src/routes/v1/index.ts
      import { Router } from "express";
      import { userRoutes } from "./user-routes";
      import { productRoutes } from "./product-routes";
      import { orderRoutes } from "./order-routes";
      
      const router = Router();
      
      router.use("/users", userRoutes);
      router.use("/products", productRoutes);
      router.use("/orders", orderRoutes);
      
      export { router as v1Routes };
      ```
      
      ```typescript
      // src/routes/index.ts
      import { Router } from "express";
      import { v1Routes } from "./v1";
      import { v2Routes } from "./v2";
      
      const router = Router();
      
      router.use("/v1", v1Routes);
      router.use("/v2", v2Routes);
      
      export { router as apiRoutes };
      ```
      
      ```typescript
      // src/app.ts
      import { apiRoutes } from "./routes";
      app.use("/api", apiRoutes);
      // Results in: /api/v1/users, /api/v2/users, etc.
      ```
      
      **Why good:** Clean versioning, nested routers for organization, single mount point in app
      
      ---
      
      ## Route-Level Middleware
      
      ### Good Example - Mixed Public/Protected Routes
      
      ```typescript
      // src/routes/post-routes.ts
      import { Router } from "express";
      import { requireAuth, optionalAuth } from "../middleware/auth-guard";
      
      const router = Router();
      
      // Public: anyone can read
      router.get("/", async (req, res, next) => {
        // No auth required
      });
      
      // Public with optional auth: shows user-specific data if logged in
      router.get("/:id", optionalAuth, async (req, res, next) => {
        const post = await getPost(req.params.id);
        const isOwner = req.user?.id === post.authorId;
        // Can show edit controls if isOwner
      });
      
      // Protected: must be logged in
      router.post("/", requireAuth, async (req, res, next) => {
        // Only authenticated users
      });
      
      export { router as postRoutes };
      ```
      
      **Why good:** `router.use(requireAuth)` applies to all routes when needed, per-route middleware for mixed access, `optionalAuth` for hybrid routes
      
      ---
      
      ## Quick Reference
      
      | Pattern                 | URL            | Description    |
      | ----------------------- | -------------- | -------------- |
      | `router.get("/")`       | /api/users     | List all       |
      | `router.get("/:id")`    | /api/users/:id | Get one        |
      | `router.post("/")`      | /api/users     | Create         |
      | `router.put("/:id")`    | /api/users/:id | Full update    |
      | `router.patch("/:id")`  | /api/users/:id | Partial update |
      | `router.delete("/:id")` | /api/users/:id | Delete         |
      
      | Status Code | Constant            | Use For           |
      | ----------- | ------------------- | ----------------- |
      | 200         | HTTP_OK             | Success with body |
      | 201         | HTTP_CREATED        | Resource created  |
      | 204         | HTTP_NO_CONTENT     | Success, no body  |
      | 400         | HTTP_BAD_REQUEST    | Client error      |
      | 404         | HTTP_NOT_FOUND      | Not found         |
      | 500         | HTTP_INTERNAL_ERROR | Server error      |
      
  • reference.md 8.4 KB
    # Express.js Reference
    
    > Decision frameworks, anti-patterns, and production checklist. Referenced from [SKILL.md](SKILL.md).
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Middleware vs Route Handler
    
    ```
    Where should this logic live?
    ├─ Is it reusable across multiple routes?
    │   ├─ YES -> Middleware
    │   └─ NO -> Route handler
    ├─ Does it modify req/res for downstream handlers?
    │   ├─ YES -> Middleware
    │   └─ NO -> Route handler
    ├─ Is it authentication/authorization?
    │   ├─ YES -> Middleware (route guard)
    │   └─ NO -> Continue evaluation
    ├─ Is it request validation?
    │   ├─ YES -> Middleware
    │   └─ NO -> Route handler
    └─ Is it error handling?
        ├─ YES -> Error middleware (4 args)
        └─ NO -> Route handler
    ```
    
    ### express.Router() vs app.METHOD()
    
    ```
    How to structure routes?
    ├─ Small app with few routes?
    │   └─ app.get/post/etc. on main app is acceptable
    ├─ Multiple resources (users, products, orders)?
    │   └─ express.Router() per resource
    ├─ Need router-specific middleware?
    │   └─ express.Router() with router.use()
    └─ Routes growing beyond 200 lines?
        └─ Split into Router modules immediately
    ```
    
    ### Error Handling Strategy
    
    ```
    How to handle this error?
    ├─ Is it a validation error?
    │   └─ Return 400 with details immediately
    ├─ Is it a "not found" error?
    │   └─ Return 404 in the route handler
    ├─ Is it authentication/authorization?
    │   └─ Return 401/403 in auth middleware
    ├─ Is it an unexpected error?
    │   └─ Call next(error) -> centralized handler
    └─ Is the error in async code?
        ├─ Express 5 -> Automatic (errors auto-forwarded)
        └─ Express 4 -> Wrap in try/catch, call next(error)
    ```
    
    ### Async Handler Approach
    
    ```
    How to handle async route handlers?
    ├─ Express 5?
    │   └─ Use async/await directly (auto-forwarding)
    ├─ Express 4 with many async routes?
    │   └─ Use asyncHandler wrapper utility
    ├─ Express 4 with few async routes?
    │   └─ Manual try/catch + next(error)
    └─ Using express-async-errors package?
        └─ Use async/await directly (patched behavior)
    ```
    
    </decision_framework>
    
    <anti_patterns>
    
    ## Anti-Patterns to Avoid
    
    ### Error Handler with Wrong Signature
    
    ```typescript
    // WRONG: Only 3 arguments - NOT an error handler
    const errorHandler = (err, req, res) => {
      res.status(500).json({ error: err.message });
    };
    
    // WRONG: Arguments in wrong order
    const errorHandler = (req, res, err, next) => {
      res.status(500).json({ error: err.message });
    };
    ```
    
    **Why it's wrong:** Express identifies error handlers by the 4-argument signature `(err, req, res, next)`. Wrong signature means Express treats it as regular middleware.
    
    **What to do instead:**
    
    ```typescript
    const HTTP_INTERNAL_ERROR = 500;
    
    const errorHandler = (
      err: Error,
      req: Request,
      res: Response,
      next: NextFunction,
    ): void => {
      if (res.headersSent) {
        next(err);
        return;
      }
      res.status(HTTP_INTERNAL_ERROR).json({ error: err.message });
    };
    ```
    
    ---
    
    ### Missing Async Error Forwarding (Express 4)
    
    ```typescript
    // WRONG: Async error not forwarded
    router.get("/:id", async (req, res) => {
      const user = await getUserById(req.params.id); // Error = unhandled rejection
      res.json({ data: user });
    });
    ```
    
    **Why it's wrong:** Express 4 doesn't catch errors from rejected promises. The request hangs until timeout.
    
    **What to do instead:**
    
    ```typescript
    // Option A: Explicit error forwarding
    router.get("/:id", async (req, res, next) => {
      try {
        const user = await getUserById(req.params.id);
        res.json({ data: user });
      } catch (error) {
        next(error);
      }
    });
    
    // Option B: Using wrapper utility
    router.get(
      "/:id",
      asyncHandler(async (req, res) => {
        const user = await getUserById(req.params.id);
        res.json({ data: user });
      }),
    );
    ```
    
    ---
    
    ### God Route Files
    
    ```typescript
    // WRONG: Everything in one file - 2000+ lines
    const app = express();
    app.get("/api/users", (req, res) => {
      /* 50 lines */
    });
    app.post("/api/users", (req, res) => {
      /* 50 lines */
    });
    app.get("/api/products", (req, res) => {
      /* 50 lines */
    });
    // ... hundreds more
    ```
    
    **Why it's wrong:** Impossible to navigate, test, or maintain. No separation of concerns.
    
    **What to do instead:**
    
    ```typescript
    // src/routes/user-routes.ts
    const router = Router();
    router.get("/" /* ... */);
    router.post("/" /* ... */);
    export { router as userRoutes };
    
    // src/app.ts
    app.use("/api/users", userRoutes);
    app.use("/api/products", productRoutes);
    ```
    
    ---
    
    ### Magic Numbers for HTTP Status
    
    ```typescript
    // WRONG: Magic numbers everywhere
    router.get("/:id", async (req, res, next) => {
      try {
        const user = await getUserById(req.params.id);
        if (!user) {
          res.status(404).json({ error: "Not found" });
          return;
        }
        res.status(200).json({ data: user });
      } catch (error) {
        res.status(500).json({ error: "Server error" });
      }
    });
    ```
    
    **What to do instead:**
    
    ```typescript
    const HTTP_OK = 200;
    const HTTP_NOT_FOUND = 404;
    
    router.get("/:id", async (req, res, next) => {
      try {
        const user = await getUserById(req.params.id);
        if (!user) {
          res.status(HTTP_NOT_FOUND).json({ error: "Not found" });
          return;
        }
        res.status(HTTP_OK).json({ data: user });
      } catch (error) {
        next(error); // Forward to centralized handler
      }
    });
    ```
    
    ---
    
    ### Insecure CORS Configuration
    
    ```typescript
    // WRONG: Wildcard origin with credentials
    app.use(
      cors({
        origin: "*",
        credentials: true, // Browsers reject this combination!
      }),
    );
    
    // WRONG: Accepting any origin dynamically
    app.use(
      cors({
        origin: (origin, callback) => {
          callback(null, true); // Allows ANY origin
        },
        credentials: true,
      }),
    );
    ```
    
    **Why it's wrong:** Browsers reject wildcard with credentials. Accepting any origin defeats CORS purpose.
    
    **What to do instead:**
    
    ```typescript
    const ALLOWED_ORIGINS = [
      "https://app.example.com",
      "https://admin.example.com",
    ];
    
    app.use(
      cors({
        origin: (origin, callback) => {
          if (!origin || ALLOWED_ORIGINS.includes(origin)) {
            callback(null, true);
          } else {
            callback(new Error("Not allowed by CORS"));
          }
        },
        credentials: true,
      }),
    );
    ```
    
    </anti_patterns>
    
    ---
    
    ## HTTP Status Code Quick Reference
    
    | Code | Constant Name          | Use Case                              |
    | ---- | ---------------------- | ------------------------------------- |
    | 200  | HTTP_OK                | Successful GET, PUT, PATCH            |
    | 201  | HTTP_CREATED           | Successful POST creating resource     |
    | 204  | HTTP_NO_CONTENT        | Successful DELETE                     |
    | 400  | HTTP_BAD_REQUEST       | Validation failure, malformed request |
    | 401  | HTTP_UNAUTHORIZED      | Missing or invalid authentication     |
    | 403  | HTTP_FORBIDDEN         | Authenticated but not authorized      |
    | 404  | HTTP_NOT_FOUND         | Resource doesn't exist                |
    | 409  | HTTP_CONFLICT          | Resource already exists (duplicate)   |
    | 422  | HTTP_UNPROCESSABLE     | Semantic validation failure           |
    | 429  | HTTP_TOO_MANY_REQUESTS | Rate limit exceeded                   |
    | 500  | HTTP_INTERNAL_ERROR    | Unexpected server error               |
    
    ---
    
    ## Production Checklist
    
    ### Before Deploying Express API
    
    **Structure:**
    
    - [ ] Routes organized with `express.Router()` by resource
    - [ ] Error handler has 4 arguments `(err, req, res, next)`
    - [ ] Error handler registered AFTER all routes
    - [ ] All async handlers forward errors (Express 4) or use Express 5
    - [ ] No God files (each file < 300 lines)
    
    **Security:**
    
    - [ ] Security headers middleware registered first
    - [ ] CORS configured with explicit origin allowlist
    - [ ] Rate limiting enabled for public endpoints
    - [ ] Request body size limited (`express.json({ limit: ... })`)
    - [ ] No wildcard CORS with credentials
    
    **Code Quality:**
    
    - [ ] HTTP status codes are named constants
    - [ ] No magic numbers
    - [ ] Named exports only (no default exports)
    - [ ] kebab-case file names
    - [ ] TypeScript types for req.body, req.params, req.query
    
    **Error Handling:**
    
    - [ ] Centralized error handler catches all errors
    - [ ] Error handler checks `res.headersSent`
    - [ ] Validation errors return 400 with details
    - [ ] 404 handler for unmatched routes
    - [ ] Errors logged with request context
    
    **Middleware Order:**
    
    - [ ] 1. Security headers
    - [ ] 2. CORS
    - [ ] 3. Rate limiting
    - [ ] 4. Body parsing
    - [ ] 5. Logging
    - [ ] 6. Routes
    - [ ] 7. 404 handler
    - [ ] 8. Error handler (last)
    
  • SKILL.md 13 KB
    ---
    name: api-framework-express
    description: Express.js routes, middleware, error handling, request/response patterns
    ---
    
    # API Development with Express.js
    
    > **Quick Guide:** Express uses middleware-based request processing. The three non-negotiable patterns: modular routing via `express.Router()`, centralized error handling with 4-argument middleware `(err, req, res, next)`, and correct middleware ordering (security first, error handler last). Express 5 (now stable, default on npm) auto-forwards async errors; Express 4 requires manual `next(err)` or a wrapper.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST define error-handling middleware with 4 arguments: `(err, req, res, next)` - Express identifies error handlers by arity)**
    
    **(You MUST register error handlers AFTER all routes and other middleware)**
    
    **(You MUST call `next(err)` to forward async errors in Express 4 - Express 5 auto-forwards rejected promises)**
    
    **(You MUST use `express.json()` and `express.urlencoded()` for body parsing - `req.body` is undefined without them)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** Express.js, express, app.use, app.get, app.post, app.put, app.delete, express.Router, req.params, req.query, req.body, res.json, res.status, middleware, next(), error handler, router.use, express.static, express.json, express.urlencoded
    
    **When to use:**
    
    - Building REST APIs with composable middleware patterns
    - Need modular route organization with `express.Router()`
    - Require centralized error handling across all routes
    - Building APIs that need body parsing, static files, or cookie handling
    - Creating route guards for authentication/authorization
    
    **When NOT to use:**
    
    - Need auto-generated OpenAPI documentation from schemas
    - Building edge/serverless functions where cold start matters
    - Need strict end-to-end type safety with schema validation
    - GraphQL APIs (use a dedicated GraphQL server)
    
    **Key patterns covered:**
    
    - Middleware chain with `app.use()` and `next()`
    - Modular routes with `express.Router()`
    - Error handling with 4-argument middleware
    - Async error forwarding (Express 4 vs 5)
    - Request validation middleware
    - Route parameters and query string handling
    - Route guards for authentication/authorization
    - Middleware ordering (security, CORS, rate limit, parsing, routes, errors)
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - App setup, error handler, async handler, body parsing
    - [examples/middleware.md](examples/middleware.md) - Logging, validation, auth guards, middleware ordering
    - [examples/routing.md](examples/routing.md) - Modular routes, parameters, response helpers, versioned APIs
    - [reference.md](reference.md) - Decision frameworks, anti-patterns, production checklist
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    **Middleware-first architecture.** Express processes requests through a chain of middleware functions. Each middleware can modify request/response objects, end the response, or call `next()` to continue the chain. Everything in Express is middleware - body parsers, auth guards, loggers, error handlers.
    
    **Express 4 vs 5:** Express 5 (stable since 2025, now default on npm) auto-forwards errors from rejected promises in async handlers. Express 4 requires explicit `try/catch` + `next(err)` or a wrapper utility. Both versions require the 4-argument signature for error handlers.
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Application Setup
    
    Register body parsers early, mount route modules, register error handler last. See [examples/core.md](examples/core.md) for full implementation.
    
    ```typescript
    const app: Express = express();
    
    // Body parsing
    app.use(express.json({ limit: JSON_LIMIT }));
    app.use(express.urlencoded({ extended: true }));
    
    // Mount route modules
    app.use("/api/users", userRoutes);
    app.use("/api/products", productRoutes);
    
    // Error handler MUST be last
    app.use(errorHandler);
    
    export { app };
    ```
    
    **Why good:** Body parsers before routes so `req.body` is populated, error handler last to catch all errors, modular route mounting
    
    ---
    
    ### Pattern 2: Modular Routes with express.Router()
    
    One Router per resource, mounted at a path prefix. See [examples/routing.md](examples/routing.md) for CRUD examples with parameters.
    
    ```typescript
    // src/routes/user-routes.ts
    const router = Router();
    
    router.get("/", async (req, res, next) => {
      try {
        const users = await getUsersFromDatabase();
        res.status(HTTP_OK).json({ data: users });
      } catch (error) {
        next(error);
      }
    });
    
    export { router as userRoutes };
    ```
    
    **Why good:** Router isolates related routes, named export, explicit error forwarding
    
    ---
    
    ### Pattern 3: Error Handling Middleware (4 Arguments)
    
    Express identifies error handlers by the 4-argument signature `(err, req, res, next)`. This is the most critical Express pattern to get right. See [examples/core.md](examples/core.md) for full implementation.
    
    ```typescript
    // CRITICAL: Must have exactly 4 arguments
    const errorHandler = (
      err: AppError,
      req: Request,
      res: Response,
      next: NextFunction,
    ): void => {
      if (res.headersSent) {
        next(err);
        return;
      }
    
      const statusCode = err.statusCode || HTTP_INTERNAL_ERROR;
      res.status(statusCode).json({
        error: { message: err.message, code: err.code || "INTERNAL_ERROR" },
      });
    };
    ```
    
    **Why good:** 4 arguments for Express to recognize as error handler, checks `headersSent` to avoid double-response, consistent error shape
    
    **Common mistake:** 3-argument function `(err, req, res)` is treated as regular middleware - `err` becomes `req`, completely wrong behavior
    
    ---
    
    ### Pattern 4: Async Error Handling
    
    Express 5 auto-forwards rejected promises. Express 4 requires explicit forwarding. See [examples/core.md](examples/core.md) for the asyncHandler wrapper.
    
    ```typescript
    // Express 5: async errors auto-forwarded
    router.get("/:id", async (req, res) => {
      const product = await getProductById(req.params.id);
      res.status(HTTP_OK).json({ data: product });
    });
    
    // Express 4: MUST forward manually
    router.get("/:id", async (req, res, next) => {
      try {
        const product = await getProductById(req.params.id);
        res.status(HTTP_OK).json({ data: product });
      } catch (error) {
        next(error); // Required in Express 4
      }
    });
    ```
    
    **Why this matters:** In Express 4, unhandled async rejections cause the request to hang until timeout. Express 5 fixes this but many projects still run Express 4.
    
    ---
    
    ### Pattern 5: Request Validation Middleware
    
    Validate `req.body` in middleware before the route handler processes it. See [examples/middleware.md](examples/middleware.md) for full validation patterns.
    
    ```typescript
    const validateUserCreate = (
      req: Request,
      res: Response,
      next: NextFunction,
    ): void => {
      const { name, email } = req.body;
      const errors: string[] = [];
    
      if (!name || name.length < MIN_NAME_LENGTH) errors.push("Name is required");
      if (!email || !email.includes("@")) errors.push("Valid email is required");
    
      if (errors.length > 0) {
        res
          .status(HTTP_BAD_REQUEST)
          .json({ error: { message: "Validation failed", details: errors } });
        return;
      }
      next();
    };
    
    // Apply: router.post("/", validateUserCreate, createHandler);
    ```
    
    **Why good:** Validation separated from business logic, early return on failure, reusable across routes
    
    ---
    
    ### Pattern 6: Route Guards (Authentication/Authorization)
    
    Protect routes with middleware that validates access. See [examples/middleware.md](examples/middleware.md) for full auth guard implementation.
    
    ```typescript
    // Extend Request with user data
    interface AuthenticatedRequest extends Request {
      user?: { id: string; role: string };
    }
    
    const requireAuth = (
      req: AuthenticatedRequest,
      res: Response,
      next: NextFunction,
    ): void => {
      const token = req.headers.authorization?.replace("Bearer ", "");
      if (!token) {
        res
          .status(HTTP_UNAUTHORIZED)
          .json({ error: { message: "Authentication required" } });
        return;
      }
      req.user = verifyToken(token);
      next();
    };
    
    // Apply to all routes in router: router.use(requireAuth);
    // Apply to specific route: router.delete("/:id", requireAuth, requireRole("admin"), handler);
    ```
    
    **Why good:** Auth middleware reusable, role guard configurable, extends Request type for type safety
    
    ---
    
    ### Pattern 7: Middleware Ordering
    
    Order matters. Security first, error handler last. See [examples/middleware.md](examples/middleware.md) for complete ordering example.
    
    ```
    1. Security headers (helmet)
    2. CORS
    3. Rate limiting (before body parsing to save resources)
    4. Body parsing (express.json, express.urlencoded)
    5. Request logging
    6. Routes
    7. 404 handler (after all routes)
    8. Error handler (LAST)
    ```
    
    **Why this order:** Security rejects bad requests early. Rate limiting before parsing saves CPU on abusive requests. Error handler must be last to catch all errors from routes.
    
    ---
    
    ### Pattern 8: Response Helpers
    
    Standardize API responses with typed helpers. See [examples/routing.md](examples/routing.md) for full implementation.
    
    ```typescript
    const sendSuccess = <T>(res: Response, data: T, statusCode = HTTP_OK): void => {
      res.status(statusCode).json({ success: true, data });
    };
    
    const sendNotFound = (res: Response, resource = "Resource"): void => {
      res
        .status(HTTP_NOT_FOUND)
        .json({ success: false, error: { message: `${resource} not found` } });
    };
    ```
    
    **Why good:** Consistent response shape across all routes, typed helpers reduce boilerplate
    
    ---
    
    ### Express 5 Migration Notes
    
    Express 5 is the default on npm since March 2025. Key changes from Express 4:
    
    | Change                | Express 4          | Express 5                                      |
    | --------------------- | ------------------ | ---------------------------------------------- |
    | Async errors          | Manual `next(err)` | Auto-forwarded                                 |
    | `req.body` (unparsed) | `{}`               | `undefined`                                    |
    | `req.query`           | Writable           | Read-only getter                               |
    | Wildcard routes       | `/*`               | `/*splat` (no root) or `/{*splat}` (with root) |
    | Optional params       | `/:file.:ext?`     | `/:file{.:ext}`                                |
    | `urlencoded` default  | `extended: true`   | `extended: false`                              |
    | `req.host`            | Strips port        | Includes port                                  |
    | Minimum Node.js       | Any                | 18+                                            |
    
    **Removed in Express 5:** `req.param()`, `res.send(body, status)`, `res.send(status)` (use `res.sendStatus()`), `res.json(obj, status)`, `res.redirect(url, status)`, `res.redirect('back')` (use `req.get('Referrer') || '/'`), `res.sendfile()` (use `res.sendFile()`), `app.del()` (use `app.delete()`).
    
    </patterns>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority:**
    
    - **Error handler has only 3 arguments** - Express treats it as regular middleware, errors silently ignored
    - **Error handler registered before routes** - Never catches route errors
    - **Missing `next(error)` in async handlers (Express 4)** - Unhandled promise rejection, request hangs
    - **Not using `express.json()` middleware** - `req.body` is undefined for JSON requests
    - **Magic HTTP status codes** - Use named constants (`HTTP_OK = 200`, `HTTP_NOT_FOUND = 404`)
    
    **Medium Priority:**
    
    - **All routes in single file** - Creates unmaintainable God file, use `express.Router()`
    - **Not checking `res.headersSent` in error handler** - Causes "headers already sent" crashes
    - **Default exports on route modules** - Violates project conventions
    - **Wildcard CORS with credentials** - Browsers reject `origin: "*"` with `credentials: true`
    - **Missing rate limiting on public APIs** - Vulnerable to abuse
    
    **Gotchas & Edge Cases:**
    
    - **`next('route')` vs `next(error)`** - String `'route'` skips to next route handler; anything else triggers error handler
    - **`req.query` values are always strings** - Parse numbers with `parseInt(val, 10)`
    - **`express.static` without auth** - Files publicly accessible unless middleware guards them
    - **Router `mergeParams: true`** - Required to access parent route params in nested routers
    - **Express 5: `req.body` is `undefined` when unparsed** - was `{}` in Express 4, may break `if (!req.body)` checks
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    **Before implementing ANY Express route, verify these requirements are met:**
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST define error-handling middleware with 4 arguments: `(err, req, res, next)` - Express identifies error handlers by arity)**
    
    **(You MUST register error handlers AFTER all routes and other middleware)**
    
    **(You MUST call `next(err)` to forward async errors in Express 4 - Express 5 auto-forwards rejected promises)**
    
    **(You MUST use `express.json()` and `express.urlencoded()` for body parsing - `req.body` is undefined without them)**
    
    **Failure to follow these rules will cause unhandled errors and broken middleware chains.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related