| 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).
<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 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 use express.json() and express.urlencoded() for body parsing)
</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)
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()
- Route guards for authentication/authorization
Detailed Resources:
- For code examples:
- middleware.md - Middleware chains, error handling, validation, auth guards
- routing.md - Modular routes, parameters, response patterns
- For decision frameworks and anti-patterns, see reference.md
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.
Use alternatives when: Need OpenAPI generation (Hono), edge deployment (Hono/Fastify), strict type safety with schema validation (tRPC).
Core Patterns
Pattern 1: Application Setup with Built-in Middleware
Set up Express with body parsing middleware and basic configuration.
Constants
const PORT = 3000;
const JSON_LIMIT = "10mb";
Implementation
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();
app.use(express.json({ limit: JSON_LIMIT }));
app.use(express.urlencoded({ extended: true }));
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use(errorHandler);
export { app };
Why good: Named constants for configuration, built-in body parsers registered early, error handler registered last, modular route mounting
const app = express();
app.use(errorHandler);
app.use(express.json({ limit: "10mb" }));
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: Modular Routes with express.Router()
Create modular, mountable route handlers for better organization.
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;
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: , : ) => {
{
{ id } = req.;
user = (id);
(!user) {
res.().({ : });
;
}
res.().({ : user });
} (error) {
(error);
}
});
router.(, (: , : , : ) => {
{
userData = req.;
newUser = (userData);
res.().({ : newUser });
} (error) {
(error);
}
});
{ router userRoutes };
Why good: Router isolates related routes, named HTTP status constants, explicit error forwarding with next(error), named export follows convention
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) => {
});
export default app;
Why bad: God file with all routes, no separation of concerns, default export violates convention
Pattern 3: Error Handling Middleware (4 Arguments)
Error handlers MUST have 4 arguments: (err, req, res, next).
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;
}
const errorHandler = (
err: AppError,
req: Request,
res: Response,
next: NextFunction,
): void => {
console.error(`[Error] ${req.method} ${req.path}:`, err.message);
if (res.headersSent) {
next(err);
return;
}
const statusCode = err.statusCode || HTTP_INTERNAL_ERROR;
const message = err.message || "Internal server error";
res.status(statusCode).({
: {
message,
: err. || ,
: req.,
},
});
};
{ errorHandler };
Why good: 4 arguments identifies this as error middleware, checks headersSent to avoid double-response, logs with request context, consistent error shape
const errorHandler = (err, req, res) => {
res.status(500).send("Error");
};
const errorHandler = (err, req, res, next) => {
res.status(500).json({ error: err.message });
};
Why bad: 3-argument function treated as regular middleware (ignores err), not checking headersSent causes "headers already sent" crashes
Pattern 4: Async Error Handling
Forward errors from async functions to the error handler.
Express 4 Pattern (Manual Forwarding)
import { Router } from "express";
import type { Request, Response, NextFunction } from "express";
const router = Router();
const HTTP_OK = 200;
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);
}
});
router.get("/featured", (req: Request, res: Response, next: NextFunction) => {
getFeaturedProducts()
.( res.().({ : products }))
.(next);
});
{ router productRoutes };
Wrapper Function for Cleaner Code
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 };
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 });
}),
);
Why good: asyncHandler removes try/catch boilerplate, errors automatically forwarded, cleaner route definitions
router.get("/:id", async (req, res) => {
const product = await getProductById(req.params.id);
res.json({ data: product });
});
Why bad: Async errors not caught by Express 4, causes unhandled promise rejection and request hangs
Pattern 5: Request Validation Middleware
Validate request data before handlers process it.
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 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.();
}
(errors. > ) {
res.().({
: {
: ,
: errors,
},
});
;
}
();
};
{ validateUserCreate };
router.post("/", validateUserCreate, async (req, res, next) => {
try {
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.
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;
}
router.get("/", async (req: Request, res: Response, next: NextFunction) => {
try {
const { q, page, limit, sort } = req.query as SearchQuery;
const pageNum = (page || (), );
limitNum = .(
(limit || (), ),
,
);
results = ({
: q || ,
: pageNum,
: limitNum,
: sort || ,
});
res.().({
: results.,
: {
: pageNum,
: limitNum,
: results.,
},
});
} (error) {
(error);
}
});
router.(
,
(: , : , : ) => {
{
{ userId, postId } = req.;
post = (userId, postId);
res.().({ : post });
} (error) {
(error);
}
},
);
{ router searchRoutes };
Why good: Query params typed, default values with named constants, parseInt with radix 10, limit capped at MAX_LIMIT to prevent abuse
Pattern 7: Route Guards (Authentication/Authorization)
Protect routes with middleware that validates access.
import type { Request, Response, NextFunction } from "express";
const HTTP_UNAUTHORIZED = 401;
const HTTP_FORBIDDEN = 403;
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;
}
try {
const user = verifyToken(token);
req.user = user;
();
} {
res.().({
: { : },
});
}
};
= () => {
(
: ,
: ,
: ,
): {
(!req.) {
res.().({
: { : },
});
;
}
(!allowedRoles.(req..)) {
res.().({
: { : },
});
;
}
();
};
};
{ requireAuth, requireRole };
{ };
import { requireAuth, requireRole } from "../middleware/auth-guard";
router.use(requireAuth);
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);
}
});
Why good: Auth middleware reusable, role-based guard configurable, extends Request type for type safety, clear separation of auth concerns
Pattern 8: Static Files and Built-in Middleware
Serve static assets and use Express built-in middleware.
import express from "express";
import path from "path";
const app = express();
const STATIC_MAX_AGE_MS = 86400000;
app.use(
express.static(path.join(__dirname, "public"), {
maxAge: STATIC_MAX_AGE_MS,
etag: true,
}),
);
app.use(
"/uploads",
requireAuth,
express.static(path.join(__dirname, "uploads")),
);
export { app };
Why good: Caching configured with named constant, path.join for cross-platform compatibility, protected static route with auth middleware
Pattern 9: Response Patterns with Consistent Shape
Standardize API responses for predictable client consumption.
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>,
): => {
: <T> = { : , data };
(meta) {
response. = meta;
}
res.(statusCode).(response);
};
sendCreated = <T>(: , : T): {
(res, data, );
};
sendNoContent = (: ): {
res.().();
};
sendError = (
: ,
: ,
: = ,
?: ,
?: ,
): {
: = {
: ,
: { message, code, details },
};
res.(statusCode).(response);
};
sendNotFound = (: , : = ): {
(res, , , );
};
{ sendSuccess, sendCreated, sendNoContent, sendError, sendNotFound };
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
Pattern 10: Middleware Order Best Practices
Order matters for security, parsing, and error handling.
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;
const RATE_LIMIT_MAX_REQUESTS = 100;
const JSON_LIMIT = "10mb";
app.use(helmet());
app.use(
cors({
origin: process.env.ALLOWED_ORIGINS?.split(",") || [],
credentials: true,
}),
);
app.use(
rateLimit({
windowMs: RATE_LIMIT_WINDOW_MS,
max: RATE_LIMIT_MAX_REQUESTS,
standardHeaders: true,
legacyHeaders: false,
}),
);
app.(express.({ : _LIMIT }));
app.(express.({ : }));
app.(requestLogger);
app.(, userRoutes);
app.(, productRoutes);
app.( {
res.().({ : { : } });
});
app.(errorHandler);
{ app };
Why good: Security (helmet) first, rate limiting before expensive body parsing, error handler absolutely last, 404 handler catches unmatched routes
Integration Guide
Works with:
- 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
Replaces / Conflicts with:
- 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
<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 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 use express.json() and express.urlencoded() for body parsing)
Failure to follow these rules will cause unhandled errors and broken middleware chains.
</critical_reminders>