Skip to content
Back to skills

Api Framework Express

ASecurity

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

  • 57 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added May 31, 2026
developmenttypescriptgoexpressapidatabasesecuritydocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned May 31, 2026

npx -y skills add diegosouzapw/awesome-omni-skill --skill api-framework-express --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Api Framework Express?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Api Framework Express
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/diegosouzapw-api-framework-express/badge)](https://www.skillsdirectory.com/skills/diegosouzapw-api-framework-express)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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](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>

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.