src/skills/api-framework-express/SKILL.md
Express.js routes, middleware, error handling, request/response patterns
npx skillsauth add agents-inc/skills api-framework-expressInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Express uses middleware-based request processing. The three non-negotiable patterns: modular routing via
express.Router(), centralized error handling with 4-argument middleware(err, req, res, next), and correct middleware ordering (security first, error handler last). Express 5 (now stable, default on npm) auto-forwards async errors; Express 4 requires manualnext(err)or a wrapper.
<critical_requirements>
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) - Express identifies error handlers by arity)
(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 auto-forwards rejected promises)
(You MUST use express.json() and express.urlencoded() for body parsing - req.body is undefined without them)
</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:
express.Router()When NOT to use:
Key patterns covered:
app.use() and next()express.Router()Detailed Resources:
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. Everything in Express is middleware - body parsers, auth guards, loggers, error handlers.
Express 4 vs 5: Express 5 (stable since 2025, now default on npm) auto-forwards errors from rejected promises in async handlers. Express 4 requires explicit try/catch + next(err) or a wrapper utility. Both versions require the 4-argument signature for error handlers.
Register body parsers early, mount route modules, register error handler last. See examples/core.md for full implementation.
const app: Express = express();
// 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: Body parsers before routes so req.body is populated, error handler last to catch all errors, modular route mounting
One Router per resource, mounted at a path prefix. See examples/routing.md for CRUD examples with parameters.
// src/routes/user-routes.ts
const router = Router();
router.get("/", async (req, res, next) => {
try {
const users = await getUsersFromDatabase();
res.status(HTTP_OK).json({ data: users });
} catch (error) {
next(error);
}
});
export { router as userRoutes };
Why good: Router isolates related routes, named export, explicit error forwarding
Express identifies error handlers by the 4-argument signature (err, req, res, next). This is the most critical Express pattern to get right. See examples/core.md for full implementation.
// CRITICAL: Must have exactly 4 arguments
const errorHandler = (
err: AppError,
req: Request,
res: Response,
next: NextFunction,
): void => {
if (res.headersSent) {
next(err);
return;
}
const statusCode = err.statusCode || HTTP_INTERNAL_ERROR;
res.status(statusCode).json({
error: { message: err.message, code: err.code || "INTERNAL_ERROR" },
});
};
Why good: 4 arguments for Express to recognize as error handler, checks headersSent to avoid double-response, consistent error shape
Common mistake: 3-argument function (err, req, res) is treated as regular middleware - err becomes req, completely wrong behavior
Express 5 auto-forwards rejected promises. Express 4 requires explicit forwarding. See examples/core.md for the asyncHandler wrapper.
// Express 5: async errors auto-forwarded
router.get("/:id", async (req, res) => {
const product = await getProductById(req.params.id);
res.status(HTTP_OK).json({ data: product });
});
// Express 4: MUST forward manually
router.get("/:id", async (req, res, next) => {
try {
const product = await getProductById(req.params.id);
res.status(HTTP_OK).json({ data: product });
} catch (error) {
next(error); // Required in Express 4
}
});
Why this matters: In Express 4, unhandled async rejections cause the request to hang until timeout. Express 5 fixes this but many projects still run Express 4.
Validate req.body in middleware before the route handler processes it. See examples/middleware.md for full validation patterns.
const validateUserCreate = (
req: Request,
res: Response,
next: NextFunction,
): void => {
const { name, email } = req.body;
const errors: string[] = [];
if (!name || name.length < MIN_NAME_LENGTH) errors.push("Name is required");
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();
};
// Apply: router.post("/", validateUserCreate, createHandler);
Why good: Validation separated from business logic, early return on failure, reusable across routes
Protect routes with middleware that validates access. See examples/middleware.md for full auth guard implementation.
// Extend Request with user data
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;
}
req.user = verifyToken(token);
next();
};
// Apply to all routes in router: router.use(requireAuth);
// Apply to specific route: router.delete("/:id", requireAuth, requireRole("admin"), handler);
Why good: Auth middleware reusable, role guard configurable, extends Request type for type safety
Order matters. Security first, error handler last. See examples/middleware.md for complete ordering example.
1. Security headers (helmet)
2. CORS
3. Rate limiting (before body parsing to save resources)
4. Body parsing (express.json, express.urlencoded)
5. Request logging
6. Routes
7. 404 handler (after all routes)
8. Error handler (LAST)
Why this order: Security rejects bad requests early. Rate limiting before parsing saves CPU on abusive requests. Error handler must be last to catch all errors from routes.
Standardize API responses with typed helpers. See examples/routing.md for full implementation.
const sendSuccess = <T>(res: Response, data: T, statusCode = HTTP_OK): void => {
res.status(statusCode).json({ success: true, data });
};
const sendNotFound = (res: Response, resource = "Resource"): void => {
res
.status(HTTP_NOT_FOUND)
.json({ success: false, error: { message: `${resource} not found` } });
};
Why good: Consistent response shape across all routes, typed helpers reduce boilerplate
Express 5 is the default on npm since March 2025. Key changes from Express 4:
| Change | Express 4 | Express 5 |
| --------------------- | ------------------ | ---------------------------------------------- |
| Async errors | Manual next(err) | Auto-forwarded |
| req.body (unparsed) | {} | undefined |
| req.query | Writable | Read-only getter |
| Wildcard routes | /* | /*splat (no root) or /{*splat} (with root) |
| Optional params | /:file.:ext? | /:file{.:ext} |
| urlencoded default | extended: true | extended: false |
| req.host | Strips port | Includes port |
| Minimum Node.js | Any | 18+ |
Removed in Express 5: req.param(), res.send(body, status), res.send(status) (use res.sendStatus()), res.json(obj, status), res.redirect(url, status), res.redirect('back') (use req.get('Referrer') || '/'), res.sendfile() (use res.sendFile()), app.del() (use app.delete()).
<red_flags>
High Priority:
next(error) in async handlers (Express 4) - Unhandled promise rejection, request hangsexpress.json() middleware - req.body is undefined for JSON requestsHTTP_OK = 200, HTTP_NOT_FOUND = 404)Medium Priority:
express.Router()res.headersSent in error handler - Causes "headers already sent" crashesorigin: "*" with credentials: trueGotchas & Edge Cases:
next('route') vs next(error) - String 'route' skips to next route handler; anything else triggers error handlerreq.query values are always strings - Parse numbers with parseInt(val, 10)express.static without auth - Files publicly accessible unless middleware guards themmergeParams: true - Required to access parent route params in nested routersreq.body is undefined when unparsed - was {} in Express 4, may break if (!req.body) checks</red_flags>
<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) - Express identifies error handlers by arity)
(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 auto-forwards rejected promises)
(You MUST use express.json() and express.urlencoded() for body parsing - req.body is undefined without them)
Failure to follow these rules will cause unhandled errors and broken middleware chains.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety