git:20260202.b236384 to git:20260316.5c62783

121 added, 597 removed. Audit A to A.

---
name: api-framework-express
description: Express.js routes, middleware, error handling, request/response patterns
---
# API Development with Express.js
- > **Quick Guide:** Use Express.js for building REST APIs with middleware-based request processing. The framework excels at composable middleware chains, modular routing via `express.Router()`, and centralized error handling with 4-argument error middleware `(err, req, res, next)`.
+ > **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)`)**
+ **(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 handles this automatically)**
+ **(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)**
+ **(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 (consider Hono + OpenAPI or Fastify + Swagger)
- - Building edge/serverless functions where cold start matters (consider Hono)
- - Need strict type safety with Zod validation built-in (consider Hono + zod-openapi)
- - GraphQL APIs (use Apollo Server or similar)
+ - 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()`
- - Routing with `app.get/post/put/delete` and route parameters
- Modular routes with `express.Router()`
- Error handling with 4-argument middleware
- - Async/await error forwarding patterns
- - Request validation and parsing
- - Response patterns with `res.json()` and `res.status()`
- - Static file serving with `express.static()`
+ - 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:**
- - For code examples:
- - [middleware.md](examples/middleware.md) - Middleware chains, error handling, validation, auth guards
- - [routing.md](examples/routing.md) - Modular routes, parameters, response patterns
- - For decision frameworks and anti-patterns, see [reference.md](reference.md)
+ - [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.
-
- **Use Express when:** Need mature ecosystem with extensive middleware library, building traditional REST APIs, team familiarity with Express patterns.
+ **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.
- **Use alternatives when:** Need OpenAPI generation (Hono), edge deployment (Hono/Fastify), strict type safety with schema validation (tRPC).
+ **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 with Built-in Middleware
-
- Set up Express with body parsing middleware and basic configuration.
-
- #### Constants
-
- ```typescript
- const PORT = 3000;
- const JSON_LIMIT = "10mb";
- ```
+ ### Pattern 1: Application Setup
- #### Implementation
+ Register body parsers early, mount route modules, register error handler last. See [examples/core.md](examples/core.md) for full implementation.
```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 app: Express = express();
- // Built-in middleware for body parsing
+ // 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
-
- ```typescript
- // WRONG: Magic strings and error handler in wrong position
- const app = express();
- app.use(errorHandler); // Error handler 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
+ **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()
- Create modular, mountable route handlers for better organization.
+ 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
- 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;
-
- // GET /api/users
- router.get("/", async (req: Request, res: Response, next: NextFunction) => {
+ router.get("/", async (req, res, next) => {
try {
const users = await getUsersFromDatabase();
res.status(HTTP_OK).json({ data: users });
} catch (error) {
- next(error); // Forward to error handler
- }
- });
-
- // GET /api/users/:id
- 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: "User not found" });
- return;
- }
-
- res.status(HTTP_OK).json({ data: user });
- } catch (error) {
next(error);
}
});
- // POST /api/users
- router.post("/", async (req: Request, res: Response, next: NextFunction) => {
- try {
- const userData = req.body;
- const newUser = await createUser(userData);
- res.status(HTTP_CREATED).json({ data: newUser });
- } catch (error) {
- next(error);
- }
- });
-
- // Named export (project convention)
export { router as userRoutes };
```
- **Why good:** Router isolates related routes, named HTTP status constants, explicit error forwarding with next(error), named export follows convention
-
- ```typescript
- // WRONG: All routes in main app file
- const app = express();
- app.get("/api/users", (req, res) => {
- /* ... */
- });
- app.get("/api/users/:id", (req, res) => {
- /* ... */
- });
- app.post("/api/users", (req, res) => {
- /* ... */
- });
- app.get("/api/products", (req, res) => {
- /* ... */
- });
- // ... hundreds more lines
-
- export default app; // Default export
- ```
-
- **Why bad:** God file with all routes, no separation of concerns, default export violates convention
+ **Why good:** Router isolates related routes, named export, explicit error forwarding
---
### Pattern 3: Error Handling Middleware (4 Arguments)
- Error handlers MUST have 4 arguments: `(err, req, res, next)`.
+ 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
- // src/middleware/error-handler.ts
- import type { Request, Response, NextFunction } from "express";
-
- const HTTP_BAD_REQUEST = 400;
- const HTTP_INTERNAL_ERROR = 500;
-
- interface AppError extends Error {
- statusCode?: number;
- code?: string;
- }
-
+ // CRITICAL: Must have exactly 4 arguments
const errorHandler = (
err: AppError,
req: Request,
res: Response,
next: NextFunction,
): void => {
- console.error(`[Error] ${req.method} ${req.path}:`, err.message);
-
- // Already sent response - delegate to default handler
if (res.headersSent) {
next(err);
return;
}
const statusCode = err.statusCode || HTTP_INTERNAL_ERROR;
- const message = err.message || "Internal server error";
-
res.status(statusCode).json({
- error: {
- message,
- code: err.code || "INTERNAL_ERROR",
- path: req.path,
- },
+ error: { message: err.message, code: err.code || "INTERNAL_ERROR" },
});
};
-
- export { errorHandler };
```
- **Why good:** 4 arguments identifies this as error middleware, checks headersSent to avoid double-response, logs with request context, consistent error shape
-
- ```typescript
- // WRONG: Only 3 arguments - Express won't recognize as error handler
- const errorHandler = (err, req, res) => {
- res.status(500).send("Error"); // Magic number
- };
-
- // WRONG: Not checking headersSent
- const errorHandler = (err, req, res, next) => {
- res.status(500).json({ error: err.message }); // May crash if headers sent
- };
- ```
+ **Why good:** 4 arguments for Express to recognize as error handler, checks `headersSent` to avoid double-response, consistent error shape
- **Why bad:** 3-argument function treated as regular middleware (ignores err), not checking headersSent causes "headers already sent" crashes
+ **Common mistake:** 3-argument function `(err, req, res)` is treated as regular middleware - `err` becomes `req`, completely wrong behavior
---
### Pattern 4: Async Error Handling
- Forward errors from async functions to the error handler.
-
- #### Express 4 Pattern (Manual Forwarding)
+ Express 5 auto-forwards rejected promises. Express 4 requires explicit forwarding. See [examples/core.md](examples/core.md) for the asyncHandler wrapper.
```typescript
- // src/routes/product-routes.ts
- import { Router } from "express";
- import type { Request, Response, NextFunction } from "express";
-
- const router = Router();
- const HTTP_OK = 200;
+ // 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 });
+ });
- // Option 1: try/catch with next(error)
- router.get("/:id", async (req: Request, res: Response, next: NextFunction) => {
+ // 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
+ 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);
- });
-
- export { router as productRoutes };
```
- #### Wrapper Function for Cleaner Code
-
- ```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 };
- ```
-
- ```typescript
- // Usage with wrapper
- 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:** asyncHandler removes try/catch boilerplate, errors automatically forwarded, cleaner route definitions
-
- ```typescript
- // WRONG: Missing error forwarding in Express 4
- router.get("/:id", async (req, res) => {
- const product = await getProductById(req.params.id); // Error = unhandled rejection
- res.json({ data: product });
- });
- ```
-
- **Why bad:** Async errors not caught by Express 4, causes unhandled promise rejection and request hangs
+ **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 request data before handlers process it.
+ Validate `req.body` in middleware before the route handler processes it. See [examples/middleware.md](examples/middleware.md) for full validation patterns.
```typescript
- // src/middleware/validate-request.ts
- import type { Request, Response, NextFunction } from "express";
-
- const HTTP_BAD_REQUEST = 400;
- const MIN_NAME_LENGTH = 1;
- const MAX_NAME_LENGTH = 100;
-
- interface UserCreateBody {
- name: string;
- email: string;
- }
-
const validateUserCreate = (
req: Request,
res: Response,
next: NextFunction,
): void => {
- const { name, email } = req.body as UserCreateBody;
+ const { name, email } = req.body;
const errors: string[] = [];
- if (!name || name.length < MIN_NAME_LENGTH || name.length > MAX_NAME_LENGTH) {
- errors.push(
- `Name must be between ${MIN_NAME_LENGTH} and ${MAX_NAME_LENGTH} characters`,
- );
- }
-
- if (!email || !email.includes("@")) {
- errors.push("Valid email is required");
- }
+ 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,
- },
- });
+ res
+ .status(HTTP_BAD_REQUEST)
+ .json({ error: { message: "Validation failed", details: errors } });
return;
}
-
next();
};
- export { validateUserCreate };
- ```
-
- ```typescript
- // Apply validation middleware to route
- router.post("/", validateUserCreate, async (req, res, next) => {
- try {
- // req.body is validated at this point
- const user = await createUser(req.body);
- res.status(HTTP_CREATED).json({ data: user });
- } catch (error) {
- next(error);
- }
- });
- ```
-
- **Why good:** Validation separated from business logic, named constants for limits, early return on validation failure, reusable across routes
-
- ---
-
- ### Pattern 6: Route Parameters and Query Strings
-
- Access dynamic URL segments and query parameters.
-
- ```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;
- }
-
- // GET /api/search?q=term&page=1&limit=20&sort=date
- router.get("/", async (req: Request, res: Response, next: NextFunction) => {
- try {
- const { q, page, limit, sort } = req.query as SearchQuery;
-
- const pageNum = parseInt(page || String(DEFAULT_PAGE), 10);
- const limitNum = Math.min(
- parseInt(limit || String(DEFAULT_LIMIT), 10),
- MAX_LIMIT,
- );
-
- const results = await searchItems({
- query: q || "",
- page: pageNum,
- limit: limitNum,
- sort: sort || "relevance",
- });
-
- res.status(HTTP_OK).json({
- data: results.items,
- pagination: {
- page: pageNum,
- limit: limitNum,
- total: results.total,
- },
- });
- } catch (error) {
- next(error);
- }
- });
-
- // Route parameters: GET /api/users/:userId/posts/:postId
- router.get(
- "/users/:userId/posts/:postId",
- async (req: Request, res: Response, next: NextFunction) => {
- try {
- const { userId, postId } = req.params;
- const post = await getPostByUserAndId(userId, postId);
- res.status(HTTP_OK).json({ data: post });
- } catch (error) {
- next(error);
- }
- },
- );
-
- export { router as searchRoutes };
+ // Apply: router.post("/", validateUserCreate, createHandler);
```
- **Why good:** Query params typed, default values with named constants, parseInt with radix 10, limit capped at MAX_LIMIT to prevent abuse
+ **Why good:** Validation separated from business logic, early return on failure, reusable across routes
---
- ### Pattern 7: Route Guards (Authentication/Authorization)
+ ### Pattern 6: Route Guards (Authentication/Authorization)
- Protect routes with middleware that validates access.
+ Protect routes with middleware that validates access. See [examples/middleware.md](examples/middleware.md) for full auth guard implementation.
```typescript
- // src/middleware/auth-guard.ts
- import type { Request, Response, NextFunction } from "express";
-
- const HTTP_UNAUTHORIZED = 401;
- const HTTP_FORBIDDEN = 403;
-
+ // Extend Request with user data
interface AuthenticatedRequest extends Request {
- user?: {
- id: string;
- role: string;
- };
+ 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" },
- });
+ res
+ .status(HTTP_UNAUTHORIZED)
+ .json({ error: { message: "Authentication required" } });
return;
}
-
- try {
- // Verify token with your auth solution
- const user = verifyToken(token);
- req.user = user;
- next();
- } catch {
- res.status(HTTP_UNAUTHORIZED).json({
- error: { message: "Invalid 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();
- };
+ req.user = verifyToken(token);
+ next();
};
- export { requireAuth, requireRole };
- export type { AuthenticatedRequest };
- ```
-
- ```typescript
- // Apply guards to routes
- import { requireAuth, requireRole } from "../middleware/auth-guard";
-
- // All routes in this router require authentication
- router.use(requireAuth);
-
- // Only admins can delete users
- router.delete("/:id", requireRole(["admin"]), async (req, res, next) => {
- try {
- await deleteUser(req.params.id);
- res.status(HTTP_OK).json({ message: "User deleted" });
- } catch (error) {
- next(error);
- }
- });
+ // 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-based guard configurable, extends Request type for type safety, clear separation of auth concerns
+ **Why good:** Auth middleware reusable, role guard configurable, extends Request type for type safety
---
- ### Pattern 8: Static Files and Built-in Middleware
-
- Serve static assets and use Express built-in middleware.
-
- ```typescript
- // src/app.ts
- import express from "express";
- import path from "path";
-
- const app = express();
-
- const STATIC_MAX_AGE_MS = 86400000; // 1 day in milliseconds
-
- // Serve static files from 'public' directory
- app.use(
- express.static(path.join(__dirname, "public"), {
- maxAge: STATIC_MAX_AGE_MS,
- etag: true,
- }),
- );
+ ### Pattern 7: Middleware Ordering
- // Serve uploaded files from 'uploads' with restricted access
- app.use(
- "/uploads",
- requireAuth, // Protect uploads
- express.static(path.join(__dirname, "uploads")),
- );
+ Order matters. Security first, error handler last. See [examples/middleware.md](examples/middleware.md) for complete ordering example.
- export { app };
```
+ 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 good:** Caching configured with named constant, path.join for cross-platform compatibility, protected static route with auth middleware
+ **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 9: Response Patterns with Consistent Shape
+ ### Pattern 8: Response Helpers
- Standardize API responses for predictable client consumption.
+ Standardize API responses with typed helpers. See [examples/routing.md](examples/routing.md) for full implementation.
```typescript
- // src/utils/response-helpers.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 sendSuccess = <T>(res: Response, data: T, statusCode = HTTP_OK): void => {
+ res.status(statusCode).json({ success: true, data });
};
- const sendNotFound = (res: Response, resource: string = "Resource"): void => {
- sendError(res, `${resource} not found`, HTTP_NOT_FOUND, "NOT_FOUND");
+ const sendNotFound = (res: Response, resource = "Resource"): void => {
+ res
+ .status(HTTP_NOT_FOUND)
+ .json({ success: false, error: { message: `${resource} not found` } });
};
-
- export { sendSuccess, sendCreated, sendNoContent, sendError, sendNotFound };
```
- ```typescript
- // Usage in routes
- import {
- sendSuccess,
- sendCreated,
- sendNotFound,
- } from "../utils/response-helpers";
-
- 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);
- }
- });
- ```
-
- **Why good:** Consistent response shape, typed responses, reusable helpers reduce boilerplate, status codes as named constants
+ **Why good:** Consistent response shape across all routes, typed helpers reduce boilerplate
---
- ### Pattern 10: Middleware Order Best Practices
-
- Order matters for security, parsing, and error handling.
-
- ```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_REQUESTS = 100;
- const JSON_LIMIT = "10mb";
-
- // 1. Security headers FIRST
- app.use(helmet());
-
- // 2. CORS configuration
- app.use(
- cors({
- origin: process.env.ALLOWED_ORIGINS?.split(",") || [],
- credentials: true,
- }),
- );
-
- // 3. Rate limiting (before body parsing to save resources)
- app.use(
- rateLimit({
- windowMs: RATE_LIMIT_WINDOW_MS,
- max: RATE_LIMIT_MAX_REQUESTS,
- standardHeaders: true,
- legacyHeaders: false,
- }),
- );
-
- // 4. Body parsing
- app.use(express.json({ limit: JSON_LIMIT }));
- app.use(express.urlencoded({ extended: true }));
-
- // 5. Request logging (after parsing, before routes)
- app.use(requestLogger);
-
- // 6. Routes
- app.use("/api/users", userRoutes);
- app.use("/api/products", productRoutes);
-
- // 7. 404 handler (after all routes)
- app.use((req, res) => {
- res.status(404).json({ error: { message: "Route not found" } });
- });
+ ### Express 5 Migration Notes
- // 8. Error handler LAST
- app.use(errorHandler);
+ Express 5 is the default on npm since March 2025. Key changes from Express 4:
- export { app };
- ```
+ | 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}` (must be named) |
+ | Optional params | `/:file.:ext?` | `/:file{.:ext}` |
+ | `urlencoded` default | `extended: true` | `extended: false` |
+ | `req.host` | Strips port | Includes port |
+ | Minimum Node.js | Any | 18+ |
- **Why good:** Security (helmet) first, rate limiting before expensive body parsing, error handler absolutely last, 404 handler catches unmatched routes
+ **Removed in Express 5:** `req.param()`, `res.send(body, status)`, `res.json(obj, status)`, `res.redirect(url, status)`, `res.sendfile()` (use `res.sendFile()`).
</patterns>
---
- <integration>
+ <red_flags>
- ## Integration Guide
+ ## RED FLAGS
- **Works with:**
+ **High Priority:**
- - **Your validation library**: Validate `req.body` in middleware before handlers
- - **Your database solution**: Query in route handlers, forward errors with `next(error)`
- - **Your authentication solution**: Implement as route guard middleware
- - **Your logging solution**: Add request/error logging middleware
+ - **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`)
- **Replaces / Conflicts with:**
+ **Medium Priority:**
- - **Hono**: Both are HTTP frameworks - choose one per project
- - **Fastify**: Both are HTTP frameworks - choose one per project
- - **Koa**: Both are HTTP frameworks - choose one per project
+ - **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
- </integration>
+ **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)`)**
+ **(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 handles this automatically)**
+ **(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)**
+ **(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>