skills/stack-shifter/skills/expressjs-rest-api

expressjs-rest-api

SKILL.md

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.

Portable references live in references/. Load only the patterns needed for the task:

  • references/app-pattern.md for Express app setup and middleware composition
  • references/route-pattern.md for route definitions with validation and auth middleware
  • references/controller-pattern.md for request handlers, error flow, and response shaping
  • references/middleware-pattern.md for auth, validation, and global error middleware
  • references/repository-pattern.md for DynamoDB repository classes
  • references/postgres-pattern.md for Postgres repository classes using Drizzle ORM
  • references/model-pattern.md for TypeScript types and Zod v4 validation schemas
  • references/error-pattern.md for typed error classes and ErrorResponseBuilder
  • references/status-code-pattern.md for the StatusCode enum
  • references/pagination-pattern.md for cursor pagination interfaces and encoding

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 still use try/catch to handle named error types before that fallback.

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.

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 (e.g. ECS, Lambda, Fly.io), environment variables are injected directly into the process — no file is loaded. 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.

Portability Rule

This skill works across repositories. Do not assume a specific folder layout exists.

Source of truth order:

  1. The target repository's existing patterns and conventions
  2. The skill's documented patterns in references/
  3. 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
  • Datastore indicators: DynamoDB SDK imports, Drizzle schema files, DATABASE_URL env var

After discovery, choose one mode and state it:

  • Existing pattern mode: extend the repository's existing abstractions
  • Pattern 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 for DynamoDB or Postgres
  • 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.

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/
│   ├── repository.interface.ts
│   ├── pagination.ts
│   ├── client.ts               ← Drizzle client (Postgres projects only)
│   └── schema.ts               ← Drizzle schema (Postgres projects only)
├── dependencies/
│   ├── aws.deps.ts             ← AWS clients: Cognito, DynamoDB, etc.
│   └── project.deps.ts         ← shared singletons: logger, repositories
├── middlewares/
├── models/
├── routes/
├── services/
└── utilities/
    ├── errors.ts
    ├── status-code.ts
    └── mappers/

Treat this as a conceptual layout, not a hard requirement.

When adding a new resource, a good flow is:

  1. model in a model or schema module
  2. route in a router module
  3. controller in a controller or handler module
  4. service method if logic is non-trivial
  5. repository in a data-access layer
  6. wire dependencies in a composition layer
  7. 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 with commonjs module output
  • Validation: Zod v4 (zod/v4)
  • Auth: Cognito JWT validation via aws-jwt-verify
  • Datastore: ask the user — DynamoDB or Postgres are both supported
    • DynamoDB: @aws-sdk/lib-dynamodb, single-table design with PK/SK keys
    • Postgres: Drizzle ORM (drizzle-orm) with postgres driver for new projects; follow the existing ORM if one is already in use
  • Security headers: helmet
  • CORS: cors with CORS_ORIGIN env var, * fallback
  • Environment loading: Node.js process.loadEnvFile() in the app bootstrap after imports when the repository does not already use another approach

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.validated instead of mutating req.query
  • thin controllers that throw typed domain errors and let global error middleware map them to HTTP responses
  • pagination ordered by stable monotonic keys, not random UUID values
  • DynamoDB Query over Scan when an access pattern is known
  • structured logging and graceful shutdown hooks for new apps
  • the repo's existing dev runner first; otherwise use tsx watch for new TypeScript projects, node --watch for compiled JavaScript, and nodemon only when custom watch behavior is already part of the project

Working Rules

  1. Read the relevant local app bootstrap, router, controller, middleware, service, and repository code before editing.
  2. 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.
  3. If the repository already has router modules, extend them. If not, generate reusable Router modules instead of putting all endpoints directly in the app bootstrap.
  4. Keep controllers thin. Put validation in middleware, orchestration in controllers, and persistence in repositories or services.
  5. 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.
  6. 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.
  7. Choose the datastore pattern deliberately: DynamoDB access patterns should drive key design; Postgres access patterns should drive schema and index design.
  8. Keep generated names, route prefixes, and response shapes aligned with the target repository's conventions.

Environment File Templates

.env.example (DynamoDB project)

NODE_ENV=
PORT=3000
CORS_ORIGIN=

# AWS
AWS_REGION=
COGNITO_USER_POOL_ID=
COGNITO_APP_CLIENT_IDS=

# DynamoDB
DYNAMODB_TABLE=

# Local development only
AWS_PROFILE=
DYNAMODB_ENDPOINT=

.env.example (Postgres project)

NODE_ENV=
PORT=3000
CORS_ORIGIN=

# AWS (if using Cognito auth)
AWS_REGION=
COGNITO_USER_POOL_ID=
COGNITO_APP_CLIENT_IDS=

# Postgres
DATABASE_URL=

.env.development (Postgres example)

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
DATABASE_URL=postgres://postgres:password@localhost:5432/myapp_dev

Response Style

When using this skill, produce:

  1. A short statement of the route(s) or component being added
  2. Whether you are in Existing pattern mode or Pattern generation mode
  3. Which datastore is in use (DynamoDB or Postgres)
  4. The planned implementation shape in order: model/schema → route → controller → service → repository → dependencies/composition → app mount
  5. Explicit assumptions where auth, table/schema shape, validation, or bootstrap wiring is ambiguous

Avoid restating generic Express or AWS documentation when a repository-specific code snippet would answer better.

Weekly Installs
1
GitHub Stars
1
First Seen
3 days ago
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1