api-framework-express
Express.js routes, middleware, error handling, request/response patterns
Best use case
api-framework-express is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Express.js routes, middleware, error handling, request/response patterns
Teams using api-framework-express should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/api-framework-express/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How api-framework-express Compares
| Feature / Agent | api-framework-express | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Express.js routes, middleware, error handling, request/response patterns
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# 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](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)
---
<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.
**Use alternatives when:** Need OpenAPI generation (Hono), edge deployment (Hono/Fastify), strict type safety with schema validation (tRPC).
</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";
```
#### 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
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
---
### Pattern 2: Modular Routes with express.Router()
Create modular, mountable route handlers for better organization.
```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) => {
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
---
### Pattern 3: Error Handling Middleware (4 Arguments)
Error handlers MUST have 4 arguments: `(err, req, res, next)`.
```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;
}
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,
},
});
};
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 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)
```typescript
// src/routes/product-routes.ts
import { Router } from "express";
import type { Request, Response, NextFunction } from "express";
const router = Router();
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);
});
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
---
### Pattern 5: Request Validation Middleware
Validate request data before handlers process it.
```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 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 (errors.length > 0) {
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 };
```
**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.
```typescript
// src/middleware/auth-guard.ts
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 {
// 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();
};
};
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);
}
});
```
**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.
```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,
}),
);
// Serve uploaded files from 'uploads' with restricted access
app.use(
"/uploads",
requireAuth, // Protect uploads
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.
```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 sendNotFound = (res: Response, resource: string = "Resource"): void => {
sendError(res, `${resource} not found`, HTTP_NOT_FOUND, "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
---
### 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" } });
});
// 8. Error handler LAST
app.use(errorHandler);
export { app };
```
**Why good:** Security (helmet) first, rate limiting before expensive body parsing, error handler absolutely last, 404 handler catches unmatched routes
</patterns>
---
<integration>
## 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
</integration>
---
<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>Related Skills
agent-framework-azure-ai-py
Build Azure AI Foundry agents using the Microsoft Agent Framework Python SDK (agent-framework-azure-ai). Use when creating persistent agents with AzureAIAgentsProvider, using hosted tools (code int...
Advanced Playwright E2E Framework
Enterprise-grade Playwright test automation framework using 8-layer architecture with Page Object Model, Module Pattern, custom fixtures, API testing layer, structured logging, data generators, multi-browser support, Docker, CI/CD pipelines, and custom HTML reporting.
Actix-web Framework
High-performance Rust web framework with actor model foundation.
account-health-framework
Use to score accounts, flag risks, and standardize remediation triggers.
9d-framework
9D product development framework
agricultural-easement-negotiation-frameworks
Expert in negotiating utility easements with farmers including farm operation impact assessment (crop production, livestock, equipment), compensation structure design (one-time vs. recurring, mitigation works), and multi-generational farm psychology. Use when negotiating transmission line, pipeline, or drainage easements with agricultural landowners. Key terms include agricultural easement, farm operation impacts, tower placement, crop loss, irrigation impacts, easement compensation, farm succession
screpcombiningexpression
Combine scTCR/BCR repertoire data with scRNA-seq expression data using `scRepertoire::combineExpression()`. This process integrates immune receptor information (CDR3 sequences, V(D)J genes, clonotypes) into a Seurat object's metadata, enabling clonotype-aware gene expression analysis.
microsoft-agent-framework
Expert guidance for implementing AI agents and multi-agent workflows using Microsoft Agent Framework. Use when adding AI agent capabilities, implementing multi-agent orchestration patterns, integrating MCP tools, or building intelligent automation systems. Emphasizes gathering up-to-date information from official documentation before implementation.
data-quality-frameworks
Implement data quality validation with Great Expectations, dbt tests, and data contracts. Use when building data quality pipelines, implementing validation rules, or establishing data contracts.
agent-os-framework
Generate standardized .agent-os directory structure with product documentation, mission, tech-stack, roadmap, and decision records. Enables AI-native workflows.
ab-test-framework-ml
Эксперт A/B тестирования. Используй для статистических тестов, экспериментов и ML-оптимизации.
startup-metrics-framework
This skill should be used when the user asks about \\\"key startup metrics", "SaaS metrics", "CAC and LTV", "unit economics", "burn multiple", "rule of 40", "marketplace metrics", or requests...