expressjs-rest-api
Purpose
Use this skill to design and implement Express 5 REST APIs in a way that fits the target repository first.
When local patterns exist, use them as the source of truth. When they do not, use the references in this skill as portable patterns to generate a coherent structure rather than one-off route code.
This skill is about Express API structure and boundaries, not database design. Persistence guidance in this skill stops at the boundary:
- controllers and services should depend on repositories, not datastore clients
- repositories should be aggregated behind one context or composition object when multiple repositories are used together
- business and domain layers should not construct datastore-specific requests directly
Portable references live in references/. Load only the patterns needed for the task:
references/app-pattern.mdfor Express app setup and middleware compositionreferences/route-pattern.mdfor route definitions with validation and auth middlewarereferences/controller-pattern.mdfor request handlers, error flow, and response shapingreferences/middleware-pattern.mdfor auth, validation, and global error middlewarereferences/model-pattern.mdfor TypeScript types and Zod v4 validation schemasreferences/error-pattern.mdfor typed error classes andErrorResponseBuilderreferences/status-code-pattern.mdfor theStatusCodeenumreferences/pagination-pattern.mdfor cursor pagination interfaces and encodingreferences/repository-pattern.mdonly for repository boundary, repository interfaces, and aggregate context guidance
Express 5 Key Behaviors
Express 5 requires Node.js 18 or higher.
For new projects, this skill's default baseline is Node.js 24 unless the target repository already uses another supported version.
Async error propagation is automatic. Rejected promises and thrown errors inside async route handlers forward to the error handler without an explicit next(error). Controllers may still use try/catch to handle named error types before that fallback when the repository already does so.
Built-in body parsing — no separate body-parser package needed:
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
Error-handling middleware must declare all four parameters (err, req, res, next) or Express treats it as regular middleware.
Route path syntax changed — review wildcard and optional segment syntax carefully when migrating Express 4 routes.
req.query should not be treated as the destination for coerced values — store validated query output on res.locals instead of mutating req.query.
When the target repository already validates only for rejection and continues reading from req.body, req.params, or req.query, preserve that pattern unless the user explicitly wants a validation-pipeline refactor.
req.body is undefined until a body parser runs — do not assume JSON payloads exist before express.json().
Environment Variable Strategy
New projects use Node.js's built-in process.loadEnvFile() instead of dotenv. Three environment files are created at project root:
| File | Purpose | Committed to git |
|---|---|---|
.env.example |
Documents all required variables with empty or example values | Yes |
.env.development |
Local development values | No |
.env.production |
Production values | No |
.gitignore must include:
.env.development
.env.production
In a composition or bootstrap module such as app.ts, import first, then load the correct file based on NODE_ENV immediately after imports and before application setup:
import express from 'express';
if (process.env.NODE_ENV !== 'production') {
process.loadEnvFile(`.env.${process.env.NODE_ENV || 'development'}`);
}
In production, environment variables are injected directly into the process. Do not attempt to load a file in production.
Avoid reading process.env at module top level inside imported files before loadEnvFile() runs. If a dependency needs env values during initialization, move that env read into a function, a factory, or the app bootstrap path.
When the target repository already relies on package scripts such as node --env-file=.env.development ..., preserve that approach instead of adding process.loadEnvFile() to the app bootstrap.
Source of Truth
This skill works across repositories. Do not assume a specific folder layout exists.
Use this order:
- the target repository's existing patterns and conventions
- the skill's documented patterns in
references/ - the inline examples in this skill
If local code differs from the skill examples, follow local code and use the examples only as design guidance.
Repository Discovery
Start every use of this skill with a short discovery pass before proposing code.
Inspect only the parts of the repository that matter for the request. Common places include:
- the Express app entry point or bootstrap module
- router modules
- controllers or route handlers
- middleware modules
- services
- repositories or data-access modules
- models and Zod schemas
- dependency wiring or composition modules
- error class hierarchy and shared response/status utilities
- pagination helpers and cursor utilities
Look for:
- how the app bootstrap is composed
- how routes are mounted and versioned
- whether controllers already exist or should be added
- whether validation middleware stores parsed output on
res.localsor only rejects invalid input - whether auth middleware already exists
- whether repositories and a repository context already exist
- whether shared services or mappers already exist for the resource area
- whether utilities already solve status codes, cursor parsing, or error handling
- style indicators such as JSDoc conventions, inline step comments, response builder usage, and route path composition
After discovery, choose one mode and state it:
Existing pattern mode: extend the repository's existing abstractionsPattern generation mode: generate the missing reusable pattern because no relevant abstraction exists
Existing Pattern Mode
Use this mode when the repo already has patterns such as:
- an app bootstrap with helmet, cors, trust proxy, and global error middleware
- router-based route definitions with inline middleware
RequestHandler-style async controllers or equivalent handlers- auth middleware for Cognito JWT or another existing auth strategy
- validation middleware for body, query, and params
- a 4-parameter global error handler
- services for business logic and external integrations
- repository classes behind a composition or context layer
- model modules with TypeScript types and Zod schemas
- typed error classes and a shared status-code utility
- dependency injection or composition root modules
Preserve local patterns even when they reflect older Express 4-era conventions. Only generate new patterns when the repository does not already establish a coherent approach.
Patterns worth preserving when present:
- routes mounted under
/v1with full resource paths declared inside each router - controller-local
try/catchblocks that map known repository errors intoErrorResponseBuilderpayloads - validation middleware that rejects invalid input without storing parsed values on
res.locals - a shared repository context or composition object that owns resource repositories
- function JSDoc and concise step comments inside handlers, middleware, and repositories
Pattern Generation Mode
Use this mode when the target repository does not already provide a reusable abstraction for one or more layers.
In this mode:
- use the references as guidance for the shape of the generated code, not as a requirement that exact files or names exist
- introduce the smallest reusable abstraction that makes future routes easier to add and maintain
- prefer generating a reusable app bootstrap, router, middleware, service, repository, or utility pattern over embedding everything in a single file
- keep generated names and folders aligned with the target repository's conventions
Preferred Project Shape
Unless the target repository already has a different established layout, use a structure like:
src/
├── app.ts
├── controllers/
├── data/
│ ├── context.ts
│ ├── repositories/
│ └── repository.interface.ts
├── dependencies/
├── middlewares/
├── models/
├── routes/
├── services/
└── utilities/
Treat this as a conceptual layout, not a hard requirement.
When adding a new resource, a good flow is:
- model in a model module
- route in a router module
- controller in a controller or handler module
- service method if logic is non-trivial
- repository in a data-access layer
- wire dependencies in a composition layer
- mount the route in the app bootstrap
Default Assumptions
- Node.js version baseline for new projects: 24
- Express version: 5.x (
express ~5.1.0) - Language: TypeScript; follow the repository's existing module system and compiler settings first
- Validation: Zod v4 (
zod/v4) - Auth: preserve the repository's existing auth strategy; if generating a new one, prefer reusable JWT/Cognito middleware over inline checks
- Security headers:
helmet - CORS:
corswithCORS_ORIGINenv var,*fallback - Environment loading: Node.js
process.loadEnvFile()in the app bootstrap after imports when the repository does not already use another approach - Module system: follow the repository first; ESM is common in modern Express 5 TypeScript projects
If the user gives constraints that conflict with these defaults, adapt and state the change.
Modern Pattern Preferences
When the target repository does not already establish a conflicting pattern, prefer:
- validated request data stored on
res.locals.validatedinstead of mutatingreq.query - thin controllers that throw typed domain errors and let global error middleware map them to HTTP responses
- repository-backed pagination with a shared cursor utility rather than ad hoc pagination per route
- structured logging and graceful shutdown hooks for new apps
- the repo's existing dev runner first; otherwise use
tsx watchfor new TypeScript projects,node --watchfor compiled JavaScript, andnodemononly when custom watch behavior is already part of the project
Working Rules
- Read the relevant local app bootstrap, router, controller, middleware, service, repository, and composition code before editing.
- If the repository already has an app bootstrap pattern, extend it. If not, generate one reusable bootstrap path instead of wiring Express ad hoc in multiple files.
- If the repository already has router modules, extend them. If not, generate reusable
Routermodules instead of putting all endpoints directly in the app bootstrap. - Keep controllers thin. Put validation in middleware, orchestration in controllers, and persistence behind repositories.
- Reuse an existing composition pattern for dependencies when it exists. If it does not, generate a lightweight composition module rather than constructing shared clients in every controller.
- Reuse a shared error and status-code pattern when one exists. If it does not, generate one shared pattern instead of repeating inline HTTP error payloads.
- Keep persistence mechanics behind repositories and one aggregate repository context. Controllers and services should not construct raw datastore requests directly.
- Keep generated names, route prefixes, and response shapes aligned with the target repository's conventions.
- Match the repository's documentation style when adding code comments: preserve JSDoc and brief step comments when those are established locally.
Environment File Templates
.env.example
NODE_ENV=
PORT=3000
CORS_ORIGIN=
# Auth / identity
AWS_REGION=
COGNITO_USER_POOL_ID=
COGNITO_APP_CLIENT_IDS=
# App-specific dependencies
APP_DEPENDENCY_CONFIG=
.env.development
NODE_ENV=development
PORT=3000
CORS_ORIGIN=http://localhost:5173
AWS_REGION=us-east-1
COGNITO_USER_POOL_ID=us-east-1_XXXXXXX
COGNITO_APP_CLIENT_IDS=your-client-id
APP_DEPENDENCY_CONFIG=local
Response Style
When using this skill, produce:
- a short statement of the route(s) or component being added
- whether you are in
Existing pattern modeorPattern generation mode - the planned implementation shape in order: model → route → controller → service → repository → dependencies/composition → app mount
- explicit assumptions where auth, validation, persistence boundary, or bootstrap wiring is ambiguous
Avoid restating generic Express or AWS documentation when a repository-specific code snippet would answer better.
More from stack-shifter/skills
spec-workflow
Drives a spec-first workflow where the spec is the locked source of truth and implementation runs through plan mode. Use whenever the user wants a feature spec drafted with explicit clarification questions, locked requirements in `docs/specs`, or a "do the next chunk then check in" implementation loop.
26cdk-rest-api-postgres
Designs and implements REST APIs on AWS using the target repository's CDK patterns first, with AWS Lambda handlers and Postgres via Drizzle as the default datastore. Use this whenever the user wants to add or modify API Gateway REST endpoints, Lambda handlers, Cognito auth, request models, reusable CDK constructs, Drizzle schema, SQL repositories, or database-backed CRUD routes, even if they only ask for "an endpoint", "a handler", "a table", "a repository", or "some CDK wiring".
12cdk-rest-api
Designs and implements REST APIs on AWS using the target repository's CDK patterns first. Use this whenever the user wants to add or modify API Gateway REST endpoints, Lambda handlers, Cognito auth, middleware, request models, reusable CDK constructs, runtime composition, scheduled jobs, or repository-backed CRUD routes, even if they only ask for "an endpoint", "a handler", "some CDK wiring", or "an API route".
1dynamodb-design
Designs DynamoDB data models from access patterns first. Use this whenever the user needs help modeling a DynamoDB schema, choosing between single-table and multi-table design, defining partition and sort keys, GSIs, item collections, one-to-many or many-to-many relationships, filtering, sorting, uniqueness, TTL, pagination, or deciding whether DynamoDB is even the right fit for the workload.
1cdk-rest-api-dynamodb
Designs and implements REST APIs on AWS using the target repository's CDK patterns first, with AWS Lambda handlers and DynamoDB as the primary datastore. Use this whenever the user wants to add or modify API Gateway REST endpoints, Lambda handlers, Cognito auth, request models, reusable CDK constructs, or DynamoDB-backed CRUD routes, even if they only ask for "an endpoint", "a handler", "a table", or "some CDK wiring".
1