Awesome-omni-skill api-framework-express
Express.js routes, middleware, error handling, request/response patterns
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/api-framework-express" ~/.claude/skills/diegosouzapw-awesome-omni-skill-api-framework-express && rm -rf "$T"
skills/development/api-framework-express/SKILL.mdAPI 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
, and centralized error handling with 4-argument error middlewareexpress.Router().(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,
, named constants)import type
(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
to forward async errors in Express 4 - Express 5 handles this automatically)next(err)
(You MUST use
and express.json()
for body parsing)express.urlencoded()
</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
andapp.use()next() - Routing with
and route parametersapp.get/post/put/delete - Modular routes with
express.Router() - Error handling with 4-argument middleware
- Async/await error forwarding patterns
- Request validation and parsing
- Response patterns with
andres.json()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>
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
const PORT = 3000; const JSON_LIMIT = "10mb";
Implementation
// 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
// 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.
// 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
// 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).
// 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
// 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)
// 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
// 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 };
// 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
// 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.
// 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 };
// 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.
// 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.
// 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 };
// 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.
// 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.
// 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 };
// 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.
// 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
in middleware before handlersreq.body - 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
to forward async errors in Express 4 - Express 5 handles this automatically)next(err)
(You MUST use
and express.json()
for body parsing)express.urlencoded()
Failure to follow these rules will cause unhandled errors and broken middleware chains.
</critical_reminders>