api-framework-express

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

16 stars

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

$curl -o ~/.claude/skills/api-framework-express/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/development/api-framework-express/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/api-framework-express/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How api-framework-express Compares

Feature / Agentapi-framework-expressStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/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

16
from diegosouzapw/awesome-omni-skill

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

16
from diegosouzapw/awesome-omni-skill

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

16
from diegosouzapw/awesome-omni-skill

High-performance Rust web framework with actor model foundation.

account-health-framework

16
from diegosouzapw/awesome-omni-skill

Use to score accounts, flag risks, and standardize remediation triggers.

9d-framework

16
from diegosouzapw/awesome-omni-skill

9D product development framework

agricultural-easement-negotiation-frameworks

16
from diegosouzapw/awesome-omni-skill

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

16
from diegosouzapw/awesome-omni-skill

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

16
from diegosouzapw/awesome-omni-skill

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

16
from diegosouzapw/awesome-omni-skill

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

16
from diegosouzapw/awesome-omni-skill

Generate standardized .agent-os directory structure with product documentation, mission, tech-stack, roadmap, and decision records. Enables AI-native workflows.

ab-test-framework-ml

16
from diegosouzapw/awesome-omni-skill

Эксперт A/B тестирования. Используй для статистических тестов, экспериментов и ML-оптимизации.

startup-metrics-framework

16
from diegosouzapw/awesome-omni-skill

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...