<!-- skill: Node.js Backend | version: 1.0.0 | description: Node 20 APIs, Express routing, middleware, config and deploy | category: Web -->

## Instructions
- Target Node 20+ LTS, set `"engines": { "node": ">=20" }`; use native ESM (`"type": "module"`), `node:` imports, and built-in test runner where possible.
- Choose Express for familiarity, Fastify for speed/validation; define routes as thin controllers delegating to `services/` (no SQL in route handlers).
- Structure: `src/index.js` (bootstrap) → `routes/` → `controllers/` → `services/` → `db/`; shared `config.js`, `logger.js`, `errors.js`, `middleware/`.
- Config via env only: `process.env` validated at boot with zod (fail fast on missing `PORT/DATABASE_URL/JWT_SECRET`); never commit `.env`, ship `.env.example`.
- Middleware order matters: helmet → cors → json parser (1mb limit) → request-id/logger → auth → routes → 404 → centralized error handler.
- Errors: custom `HttpError(status, message)` hierarchy, `asyncHandler(fn)` wrapper or express-async-errors, always `next(err)`; log with correlation IDs.
- Auth: bcrypt/argon2 password hashing, short-lived JWT access + rotating refresh tokens in httpOnly cookies, rate-limit login routes.
- Data: parameterized queries/ORM (Prisma/Drizzle); paginate lists (`?page&limit`, max 100), return `{ data, meta }`, validate bodies with zod/joi.
- Harden: `helmet`, strict CORS allowlist, `express-rate-limit`, input size caps, `npm audit`, keep dependencies minimal and pinned via lockfile.
- Scripts: `dev` (watch), `start` (prod), `test`, `lint`, `migrate`, `seed`; health endpoint `GET /health` returning version/uptime for monitors.
- Deploy: PM2 (`ecosystem.config.js`, cluster mode) or systemd unit behind Caddy/Nginx reverse proxy; zero-downtime via `pm2 reload`.
- Observability: structured JSON logs (pino), graceful shutdown on SIGTERM, expose `/metrics` if Prometheus is used; alert on 5xx rate + latency p99.

## Examples
- REST todos API: `GET/POST /api/todos` + `GET/PATCH/DELETE /api/todos/:id` with zod validation, `{ data, meta }` pagination, centralized errors.
- Auth slice: register/login with bcrypt + JWT cookies, `requireAuth` middleware, rate-limited `/login`, logout clearing refresh token.
- Deploy setup: PM2 cluster `ecosystem.config.js` + Caddy reverse proxy + systemd autostart, `.env` on server only, health-checked reloads.

## Workflows
1. Scaffold: `npm init -y`, set ESM + engines, install express/helmet/cors/pino/zod, add `src/` layout + `.env.example`.
2. Build vertical slice: config validation → health route → one resource CRUD → validation → error handler → manual curl tests.
3. Add auth + hardening: hashing, JWT middleware, helmet/cors/rate-limit, 404 + error middleware last.
4. Test + scripts: happy-path + 401/422 cases (`node --test`), `npm run dev/start/test/lint` all working.
5. Deploy: `npm ci --omit=dev` on server, env file in place, `pm2 start ecosystem.config.js`, reverse-proxy + HTTPS, verify `/health`.

## Templates
```js
// src/index.js — express bootstrap
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import { config } from './config.js';
const app = express();
app.use(helmet());
app.use(cors({ origin: config.corsOrigins }));
app.use(express.json({ limit: '1mb' }));
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
app.get('/health', (req, res) => res.json({ ok: true, version: '1.0.0' }));
app.use('/api/todos', (await import('./routes/todos.js')).default);
app.use((req, res) => res.status(404).json({ error: 'Not found' }));
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
  console.error({ err, id: req.id });
  res.status(err.status || 500).json({ error: err.message || 'Server error' });
});
app.listen(config.port, () => console.log(`Listening on :${config.port}`));
```
```js
// ecosystem.config.js — PM2 cluster
export default {
  apps: [{ name: 'api', script: 'src/index.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production' } }],
};
```

## Tools
- SARAH `shell_run`: `npm run dev` (watch), `node --test` to run tests, `pm2 status/logs` on servers.
- SARAH `file_write`: routes in `src/routes/*.js`, config in `src/config.js`, never edit `node_modules` or committed `.env`.
- SARAH preview: curl `GET /health` and one CRUD round-trip; confirm JSON shapes and status codes before deploy.

## Advanced
- Async errors: Express 4 needs wrapper (`const ah = fn => (req,res,next) => fn(req,res,next).catch(next)`); Express 5 handles async natively.
- Graceful shutdown: listen SIGTERM/SIGINT, `server.close()` + drain DB pool within 10s; PM2 `kill_timeout: 10000`.
- Troubleshooting: `EADDRINUSE` → kill stale pm2/node; CORS blocked → server `Access-Control-Allow-Origin`, not client code; empty `req.body` → missing `express.json()` before routes.
- Perf: enable compression, `etag`, connection pooling (max 10-20), index DB foreign keys; cache GETs with `Cache-Control: private, max-age=60` where safe.
- Pro tip: validate env at boot (`z.object({ PORT: z.coerce.number().default(3000) }).parse(process.env)`) so misconfig fails in seconds, not at 3am.

## Documentation
- Node.js docs: https://nodejs.org/en/docs
- Express guide: https://expressjs.com/en/starter/installing.html
- Fastify docs: https://fastify.dev/docs/latest
