Claude Skill

ia-nodejs-backend

Node.js backend patterns: layered architecture, TypeScript, validation, error handling, security, observability, logging, metrics, deployment. Use when building REST APIs, REST endpoints, middleware, Express/Fastify/Hono/NestJS/Koa servers, tRPC procedures, Bun servers, or server

LLM Mart · 0 points · 8 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download iliaal-whetstone-plugins_whetstone_skills_ia-nodejs-backend-0a409ba.zip · 19 KB
Part of iliaal/whetstone — 62 skills

Install

skills CLI npx skills add https://github.com/iliaal/whetstone/tree/master/plugins/whetstone/skills/ia-nodejs-backend
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install iliaal-whetstone@llmmart
Git git clone https://github.com/iliaal/whetstone.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole iliaal/whetstone collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Node.js Backend

Verify before implementing: For framework-specific APIs (Express 5, Fastify 5, Node.js 22+ built-ins), look up current docs via Context7 (query-docs) before writing code. Training data may lag current releases.

Working rules

  • Validate request and third-party data before use; keep response serialization and error envelopes explicit.
  • Preserve caller-visible contracts and authorization when adding resilience or fallbacks.
  • Bound concurrency, set timeouts, and avoid blocking production request paths.
  • Verify actual resource identity before parsing or caching a reused client's result.
  • Exercise operational telemetry and failure paths, not successful return codes alone.

Architecture

src/
├── routes/          # HTTP: parse request, call service, format response
├── middleware/       # Auth, validation, rate limiting, logging
├── services/        # Business logic (no HTTP types)
├── repositories/    # Data access only (queries, ORM)
├── config/          # Env, DB pool, constants
└── types/           # Shared TypeScript interfaces
  • Routes never contain business logic
  • Services never import Request/Response
  • Repositories never throw HTTP errors
  • Dependencies point inward only (Clean Architecture rule): routes -> services -> repositories. Never the reverse.
  • For scripts/prototypes: single file is fine -- ask "will this grow?"

TypeScript Rules

  • Use import type { } for type-only imports -- eliminates runtime overhead
  • Prefer interface for object shapes (2-5x faster type resolution than intersections)
  • Prefer unknown over any -- forces explicit narrowing
  • Use z.infer<typeof Schema> as single source of truth -- never duplicate types and schemas
  • Minimize as assertions -- use type guards instead
  • Add explicit return types to exported functions (faster declaration emit)
  • Untyped package? declare module 'pkg' { const v: unknown; export default v; } in types/ambient.d.ts

Discipline

  • Simplicity first -- every change as simple as possible, impact minimal code
  • Only touch what's necessary -- avoid introducing unrelated changes
  • No hacky workarounds -- if a fix feels wrong, step back and implement the clean solution
  • Before adding a new abstraction, verify it appears in 3+ places. If not, inline it.
  • If a fix requires bypassing TypeScript (as any, non-null assertions on untrusted data, // @ts-ignore), treat it as a design smell and find the typed solution

Verify

  • tsc --noEmit passes with zero errors
  • npm test passes with zero failures
  • No TypeScript bypasses (as any, @ts-ignore) in new code

References

Task-specific references

Read the relevant reference before implementing or reviewing the matching behavior:

  • For framework choice, input validation, API contracts, or errors: api-boundaries.md.
  • For concurrency, networking, startup, caches, lifecycle cleanup, or telemetry: async-and-production.md.
  • For span kinds, HTTP-status-to-span-status rules, sampling placement, metric cardinality, or telemetry data governance: observability-tracing.md.

Existing specialized references, when the corresponding topic applies:

Files (whetstone)
  • references
    • api-boundaries.md 4.8 KB
      # API boundaries
      
      ## Framework Selection
      
      | Context | Choose | Why |
      |---------|--------|-----|
      | Edge/Serverless | Hono | Zero-dep, fastest cold starts |
      | Performance API | Fastify | Higher throughput than Express, built-in schema validation |
      | Enterprise/team | NestJS | DI, decorators, structured conventions |
      | Legacy/ecosystem | Express | Most middleware, widest adoption |
      
      Ask user: deployment target, cold start needs, team experience, existing codebase.
      
      
      ## Validation
      
      **Zod** (TypeScript inference) or **TypeBox** (Fastify native). Validate at boundaries only: request entry, before DB ops, env vars at startup. Use `.extend()`, `.pick()`, `.omit()`, `.partial()`, `.merge()` for DRY schemas.
      
      - **`z.coerce.boolean()` is `Boolean(v)`.** Every non-empty string is truthy, so the literal strings `"false"`, `"0"`, `"no"` and `"off"` all coerce to `true`; only `""` and a real boolean `false` yield `false`. Clients and LLM callers routinely emit booleans as JSON strings, and the advertised schema saying `type: boolean` does not stop a host that forwards arguments unvalidated. The damage concentrates exactly where it is worst: a default-true flag can be forced on but never string-off, and a destructive flag (`kill_existing`, `force`, `active`) passed `"false"` fires. Use plain `z.boolean()` where fail-loud is acceptable, or `z.preprocess` the known spellings before `z.boolean()` so unrecognized strings still reject rather than silently becoming `true`. `.optional()` short-circuits `undefined` before the preprocess, so optional params still default correctly, and JSON Schema generation still emits `{ type: "boolean" }`.
      - **Zod v4 removed the single-argument `z.record(valueType)`** -- it requires `z.record(keyType, valueType)`, e.g. `z.record(z.string(), z.number())`. TypeScript rejects the single-arg form immediately (`tsc`: `Expected 2-3 arguments, but got 1`). If the type error is suppressed, the lone argument becomes the KEY schema and `valueType` stays `undefined`, so the first `.parse()` on a non-empty object throws `TypeError: Cannot read properties of undefined (reading '_zod')` — a raw TypeError, not a Zod validation error.
      
      
      ## Error Handling
      
      Custom error hierarchy: `AppError(message, statusCode, isOperational)` → `ValidationError(400)`, `NotFoundError(404)`, `UnauthorizedError(401)`, `ForbiddenError(403)`, `ConflictError(409)`
      
      Centralized handler middleware:
      - `AppError` → return `{ error: message }` with statusCode
      - Unknown → log full stack, return 500 + generic message in production
      - Async wrapper: `const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);`
      
      Codes: 400 bad input | 401 no auth | 403 no permission | 404 missing | 409 conflict | 422 business rule | 429 rate limited | 500 server fault
      
      
      ## API Design
      
      **Contract-first**: define route schemas (Zod schemas, Fastify JSON Schema, or OpenAPI spec) before writing handler logic. The schema is the contract -- implementation follows. Generate OpenAPI/Swagger docs from these schemas for interactive API documentation.
      
      - **Hyrum's Law awareness**: every observable response field, ordering, or timing becomes a dependency for callers. Use Zod schemas or Fastify response schemas to control exactly what's serialized -- never return raw ORM objects or untyped objects from handlers.
      - **Addition over modification**: add new optional fields rather than changing or removing existing ones. Removing a field from a response schema breaks callers silently. Deprecate first (mark in OpenAPI spec), remove in a later version.
      - **Consistent error envelope**: all errors -- validation, auth, not-found, application -- must produce the same `{ error: { code, message, details? } }` structure. Centralize in the error handler middleware. Callers build error handling once; inconsistent errors force per-endpoint special cases.
      - **Boundary validation**: validate at the middleware/route handler level (Zod `.parse()` on request body/params, Fastify schema validation). Services and repositories trust that input was validated at entry -- no redundant checks scattered through business logic.
      - **Third-party responses are untrusted data**: validate shape and content of external API responses before using them in logic, rendering, or decision-making. A compromised or misbehaving service can return unexpected types, malicious content, or missing fields. Parse through a Zod schema before use.
      - **Resources**: plural nouns (`/users`), max 2 nesting levels (`/users/:id/orders`)
      - **Methods**: GET read | POST create | PUT replace | PATCH partial | DELETE remove
      - **Versioning**: URL path `/api/v1/`
      - **Response**: `{ data, pagination?: { page, limit, total, totalPages } }`
      - **Queries**: `?page=1&limit=20&status=active&sort=createdAt,desc`
      - Return `Location` header on 201. Use 204 for successful DELETE with no body.
      
    • api-design.md 4.4 KB
      # API Design Patterns
      
      > When to read: when designing a REST or RPC endpoint surface — pagination, error envelopes, idempotency, versioning, contract-first vs code-first.
      
      ## Pagination
      
      | Use case | Type | Why |
      |----------|------|-----|
      | Admin dashboards, <10K rows | Offset (`?page=2&limit=20`) | Users expect page numbers |
      | Infinite scroll, feeds, large datasets | Cursor (`?cursor=abc&limit=20`) | Stable under concurrent writes |
      | Search results | Offset | Users need "page 3 of 12" |
      
      **Cursor implementation:**
      ```sql
      SELECT * FROM items
      WHERE id > :cursor_id
      ORDER BY id ASC
      LIMIT :limit + 1;  -- fetch N+1 to determine has_next
      ```
      
      Response: `{ data, pagination: { next_cursor, has_next } }`. Encode cursor as opaque base64 to prevent client manipulation.
      
      ## Filtering
      
      Bracket notation for comparison operators:
      ```
      ?price[gte]=10&price[lte]=100
      ?status[in]=active,pending
      ?customer.country=US          # dot notation for nested fields
      ```
      
      Comma-separated for multi-value equality:
      ```
      ?category=electronics,clothing
      ```
      
      ## Sorting
      
      Prefix `-` for descending, comma-separated for multi-field:
      ```
      ?sort=-created_at,name        # newest first, then alphabetical
      ```
      
      ## Sparse Fieldsets
      
      ```
      ?fields=id,name,email         # return only these fields
      ```
      
      ## Deprecation Protocol
      
      1. Add `Sunset` header with retirement date: `Sunset: Sat, 01 Jan 2028 00:00:00 GMT`
      2. Minimum 6-month notice before removal
      3. After sunset: return `410 Gone` with migration guidance
      
      **Breaking vs non-breaking changes:**
      
      | Non-breaking (no new version) | Breaking (requires new version) |
      |-------------------------------|--------------------------------|
      | Adding optional fields/params | Removing or renaming fields |
      | Adding new endpoints | Changing field types |
      | Adding new enum values | Removing endpoints |
      | Relaxing validation | Tightening validation |
      | Extending response with new keys | Changing response structure |
      
      ## Pre-Ship Endpoint Checklist
      
      Before shipping any new endpoint, verify:
      
      - [ ] Resource naming: plural nouns, max 2 nesting levels
      - [ ] HTTP method matches semantics (GET reads, POST creates, etc.)
      - [ ] Status codes correct (201 + Location on create, 204 on delete, 404 vs 400 distinction)
      - [ ] Request validation with schema (rejects invalid input with 400 + detail)
      - [ ] Response schema defined (controls serialized fields, no raw objects)
      - [ ] Pagination on list endpoints (cursor or offset with has_next)
      - [ ] Auth/authz enforced (401 vs 403 distinction)
      - [ ] Rate limiting configured
      - [ ] Error envelope matches project standard
      - [ ] Idempotency for non-safe methods (POST with idempotency key where needed) -- see Idempotency Keys below
      - [ ] External API responses validated before use
      - [ ] OpenAPI/docs updated
      
      ## Idempotency Keys
      
      Accepting an `Idempotency-Key` header is the easy half. Four things decide whether it works:
      
      - **Derive the key from intent, not from the attempt.** `charge:v1:${orderId}` is stable across retries; `randomUUID()` or a timestamp generated per attempt gives every retry a fresh key and dedupes nothing. If the caller supplies the key, the caller has the same obligation -- document it.
      - **Claim the key atomically.** `INSERT` the key and let a unique constraint reject the duplicate. A read-to-check-then-write is the exact race the header exists to close: two concurrent retries both read "unused" and both proceed.
      - **Reject key reuse with a different payload.** Store a hash of the request body beside the key and return 422 on mismatch. Without it, a client bug that reuses one key for two different charges gets the first charge's response for both, and the second charge silently never happens.
      - **Decide what an in-flight duplicate gets.** The first request holds the claim and has not finished. Pick one and state it: 409 and let the client retry, block on the claim and return the same response, or 202 with a status URL. Leaving it undefined means the second request usually falls through and double-executes.
      
      Treat every outbound call as three-way -- success, failure, and **unknown** (timeout, connection reset after the request was sent). Record the intent before calling out, so an unknown outcome can be reconciled rather than guessed at. Set key retention to outlive the longest path that can replay the request, including a dead-letter queue drained days later; sizing it by storage cost rather than by replay window is how a "already processed" guarantee expires early.
      
    • async-and-production.md 6.9 KB
      # Async operations and production
      
      ## Async Patterns
      
      | Pattern | Use When |
      |---------|----------|
      | `async/await` | Sequential operations |
      | `Promise.all` | Parallel independent ops |
      | `Promise.allSettled` | Parallel, some may fail |
      | `Promise.race` | Timeout or first-wins |
      
      Never use readFileSync or other sync methods in production -- use `fs.promises` or stream equivalents. Offload CPU work to worker threads (Piscina). Stream large payloads.
      
      
      ## Production Resilience
      
      - **Fail-fast env validation**: parse and validate all environment variables at startup with a Zod schema (`const env = envSchema.parse(process.env)`). If invalid, crash before serving traffic. Never discover a missing env var on the first request that needs it.
      - **Health endpoints**: expose both `/health` (shallow, always 200 if process is alive) and `/ready` (deep, verifies database, cache, and critical dependencies are reachable). Load balancers probe `/ready` for traffic routing; monitoring probes `/health` for process liveness. Don't conflate them.
      - **Caching**: Redis cache-aside for DB/API responses; in-memory LRU with TTL for hot paths. Always invalidate on writes.
      - **Load shedding**: `@fastify/under-pressure` (or equivalent) -- monitor event loop delay, heap, RSS; return 503 when thresholds exceeded.
      - **Response schemas**: In Fastify, always define response schemas -- enables `fast-json-stringify` for 2-3x faster serialization.
      - **Circuit breaker**: use `opossum` for outbound service calls. States: CLOSED (normal) -> OPEN (failing, return fallback) -> HALF_OPEN (probe). Prevents cascade failures when downstream services are down. When the outbound call *is* the security decision (authz check, trust score, license or entitlement gate), the fallback must be **deny**, and any fail-open allowance scopes to transport failure only -- connection refused, DNS failure, timeout. A response that arrived but cannot be trusted (4xx/5xx, malformed JSON, schema-invalid body, unknown verdict value) stays blocked: the endpoint was reached and did not answer. Absence of evidence is not evidence of trust. Same for "no history yet" states -- reject by default, allow only through an explicit onboarding opt-in.
      - **Node's global `fetch` (undici) drops long-silent responses.** A request that returns zero bytes for tens of seconds -- a reasoning LLM call, a slow report generator, a buffering gateway -- fails as `Invalid response body ... Premature close` whenever the egress path reaps idle TCP flows (cloud NAT, stateful firewall). `curl` and Node's built-in `https` module survive the identical request on the same box because they keep the flow warm. Rule out the red herrings before redesigning: it fails on the first call of a fresh process (not pool reuse), at concurrency 1 (not concurrency), and with `stream: true` yielding zero chunks (streaming does not help when the upstream buffers before its first byte). An SDK's `httpAgent`/`https.Agent` option is silently ignored once the SDK is on global `fetch`. Route that one request over Node's built-in `https` module with `req.on('socket', s => s.setKeepAlive(true, 10_000))` and an explicit `req.setTimeout(...)`, keeping the request/response contract identical. It works on a laptop and fails only on the deployed box -- reproduce on the host that fails.
      - **Guard the empty result set before shipping the artifact.** If every unit in an unattended pipeline failed, alert -- do not emit or email a hollow report. "The call returned without throwing" is not "I have content", and the input-side twin matters equally: a stage fed an empty series should throw rather than pass nothing downstream. Pair it with logging the *real* upstream error on each retry and on final give-up; a wrapper that prints only `attempt N failed` hides the one string ("Premature close" vs "401" vs "timeout") that names the failure class.
      - **A loop that reuses one stateful client and swallows a failed navigation attributes stale state to the current key.** `page.goto(url).catch(() => null)` inside a scraper loop parses whatever is still loaded -- the *previous* item's DOM -- and writes the extraction under the *current* item's cache key. Nothing throws, extraction "succeeds", and with a TTL the poisoned row outlives the blip that caused it; a first-item failure caches the landing page as data. Keep the `.catch` for uniform timeout handling but gate the parse and the cache write on post-conditions that confirm the right resource is loaded: the resolved URL contains the item's own path segment (compare case-insensitively -- redirects normalize slug case), and a selector present on every valid target page is in the result (this catches the URL-preserving cases: interstitials, soft-404s, layout changes). Throw on either miss so the existing per-item catch drives retry or skip, and the cache write is unreachable.
      - **A listener on a caller-supplied server outlives the module's own teardown.** Closing a server the module created also drops its listeners, but a server passed in by the caller keeps them after the module disposes -- the module must keep the handler reference and call `.off(event, handler)` in its own dispose path. Skip this and the stale listener keeps firing on the shared server, swallowing connections or messages meant for the next instance.
      
      
      ## Observability
      
      - **Define "working" before instrumenting**: write the questions an on-call engineer will ask when this is broken at 3am ("which dependency is timing out?", "is it all users or one tenant?"), then add only the telemetry that answers them. Instrumentation with no question behind it is cost and noise.
      - **Pick the signal by the question it answers**: logs = "what happened in this one case?" (high-detail, structured, sampled under load); metrics = "how often / how fast / how saturated?" (cheap aggregates — keep label cardinality bounded, never user IDs or request IDs as labels; see [observability-tracing.md](./observability-tracing.md) for the series-count budget formula and the never-instrument list); traces = "where did the time or the error go across services?".
      - **Structured logging**: `pino` with a stable set of event names and a correlation/request ID propagated through async context (`AsyncLocalStorage`). Never `console.log` in production paths.
      - **Metrics**: `prom-client` for RED per route — Rate (request count), Errors (error count), Duration (latency histogram). OpenTelemetry Node SDK for distributed traces across services.
      - **Initialize tracing before app imports, then verify it fires**: the OTel SDK must start before the modules it instruments are required, or auto-instrumentation silently no-ops. Before relying on any signal, force an error and send test traffic in staging and confirm the log/metric/trace actually lands — untested instrumentation fails silent.
      - **Alert on symptoms, not causes**: page on user-visible symptoms (error-rate spike, latency SLO burn, `/ready` flapping), not on causes (CPU high, heap growing). A cause with no symptom is a dashboard, not a page.
      
    • database-production.md 2.5 KB
      # Database & Production
      
      > When to read: when picking an ORM, configuring connection pooling, planning a migration, or hardening a Node service for production deploy.
      
      ## Database
      
      ORM: **Drizzle** (SQL-like, lightweight) or **Prisma** (schema-first, migrations built-in)
      
      Connection pooling: `new Pool({ max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000 })`
      
      Transactions: `BEGIN` → ops → `COMMIT` / catch → `ROLLBACK` / finally → `client.release()`
      
      64-bit integer columns (`BIGINT` primary keys): configure the driver to return them as `string` or `BigInt` *before* any JS code touches the value, and keep them strings across the API boundary. Ordering is the whole point -- casting to string in the serializer, after the driver has already produced a JS `Number`, does not restore the lost digits; precision is gone at parse time. `node-postgres` returns `int8` as a string by default; `mysql2` returns a `Number` unless `supportBigNumbers` and `bigNumberStrings` are set. Nothing throws -- IDs past 2^53 just come back with wrong low digits, surfacing much later as "record not found".
      
      Index strategies:
      ```sql
      CREATE INDEX idx_col ON t(col);                            -- equality
      CREATE INDEX idx_multi ON t(col1, col2);                   -- composite
      CREATE INDEX idx_partial ON t(col) WHERE status = 'active'; -- filtered
      CREATE INDEX idx_cover ON t(col) INCLUDE (name);            -- covering
      ```
      
      Always `EXPLAIN ANALYZE` slow queries. Watch for sequential scans on large tables.
      
      **Migrations:**
      - Separate schema and data migrations -- data backfills in their own migration file
      - Renames/removals use expand-contract: add new column → backfill → switch reads → drop old (see `ia-postgresql` skill for the full pattern)
      - Never edit a migration that has already run in a shared environment
      - Kysely: always type migrations as `Kysely<any>`, not your app's typed DB interface -- migrations are frozen in time and the schema will evolve past them
      - Drizzle/Prisma: keep migration SQL files under version control, review generated SQL before applying
      
      ## Production
      
      - **Docker**: multi-stage build -- `node:20-alpine` builder + prod image with `npm ci --omit=dev`
      - **Process**: PM2 cluster mode (`instances: 'max'`) or container orchestration
      - **Shutdown**: SIGTERM → stop accepting connections → drain in-flight → close DB pool
      - **Logging**: Pino (structured JSON), not console.log
      - **Health**: `GET /health` returning `{ status: 'ok' }`
      - **Compression**: gzip/brotli via middleware
      
    • observability-tracing.md 3.7 KB
      # Observability: tracing, sampling, and telemetry governance
      
      Read this when adding or reviewing OpenTelemetry spans, choosing where sampling happens, budgeting metric labels, or deciding which request data may enter telemetry at all. Signal selection, RED metrics, and structured logging live in [async-and-production.md](./async-and-production.md).
      
      ## Span kind
      
      Pick the kind by the relationship to the remote side, not by the layer of the code:
      
      - **SERVER** -- handling an inbound request while the caller waits (HTTP handler, gRPC method).
      - **CLIENT** -- an outbound call where this process waits for the answer (HTTP fetch, DB query, cache lookup).
      - **PRODUCER** -- enqueuing or scheduling work whose outcome this span does not wait for (publish to SQS/Kafka, `queue.add()`).
      - **CONSUMER** -- processing work a producer handed off (job handler, message listener).
      - **INTERNAL** -- in-process work with no remote parent or child (default). Service-layer methods are INTERNAL; do not mark them SERVER because they run "inside the server".
      
      ## HTTP status to span status
      
      The rule is asymmetric by span kind, and the common mistake is marking every 4xx as an error:
      
      - 1xx/2xx/3xx: leave span status UNSET on both kinds. Set ERROR only when a transport or protocol failure occurred (connection reset, redirect limit exceeded).
      - 4xx: on a **SERVER** span, leave status UNSET -- the server behaved correctly by rejecting the request. On the matching **CLIENT** span, set ERROR -- this process sent a request the remote refused.
      - 5xx (and any status the client cannot interpret): set ERROR on both kinds.
      - Omit the status description when `http.response.status_code` already says why; put the status code number (as a string) in `error.type`.
      - A request the caller cancelled on purpose (`AbortSignal`) is not an error: leave status UNSET and do not set `error.type`.
      
      ## Sample in the Collector, not in the SDK
      
      Leave the application SDK on `AlwaysOn` (the default is `ParentBased(root=AlwaysOn)`) and make every sampling decision in the OpenTelemetry Collector (tail sampling). An SDK-side ratio sampler (`OTEL_TRACES_SAMPLER=traceidratio`) decides at span start, before latency or outcome is known, so it drops slow and failed traces at the same rate as healthy ones; the Collector sees the finished trace and can keep every error and every slow trace while downsampling the rest. If SDK sampling is unavoidable, keep a `parentbased_*` sampler so a trace is never half-sampled across services.
      
      ## Metric cardinality budget
      
      Series count for one metric = product of the distinct value counts of every attribute x number of emitting instances. `http.request.method` (8) x `http.route` (60) x `http.response.status_code` (15) x 12 pods = 86,400 series for one histogram before bucket multiplication. Rough guide per metric: under 1,000 is free; 10,000 is normal for a fleet-wide request histogram; 100,000 needs a named owner and a backend cost check; any unbounded attribute (user ID, request ID, raw URL path, e-mail, session token, free text) is a defect at any scale because its value set grows with traffic, not with the code. Use `http.route` (the matched template), never the concrete path.
      
      ## Never-instrument list
      
      These must not appear in span attributes, metric labels, log fields, or baggage under any name, hashed or truncated included: credentials and passwords; API keys, session tokens, `Authorization`/`Cookie`/`Set-Cookie` header values; payment card numbers, CVV, bank account numbers; government identifiers (SSN, passport, tax ID); health records and diagnoses; biometric data. Capture headers by allowlist only, never "all headers minus a denylist". Request and response bodies stay off; when a body field is genuinely needed, record a redacted, schema-validated projection, not the raw payload.
      
    • security.md 4 KB
      # Authentication & Security
      
      For comprehensive security auditing (OWASP compliance, vulnerability scanning, checklist), use the `ia-security-sentinel` agent. This reference covers Node.js-specific tooling and patterns only.
      
      ## Authentication Pattern
      
      - **Access token**: JWT, 15min expiry, payload: `{ userId, email }`
      - **Refresh token**: JWT, 7d expiry, stored in DB (revocable)
      - **Passwords**: bcrypt (10+ rounds) or argon2
      - **Middleware**: extract `Bearer` token → `jwt.verify` → attach `req.user` → `next()`
      - **Authorization**: after auth, check role or resource ownership per request
      - Always return generic "Invalid credentials" -- never reveal if user exists
      
      ## Node.js Security Tooling
      
      | Concern | Tool/Package | Usage |
      |---------|-------------|-------|
      | Input validation | Zod / TypeBox | Validate at route boundary |
      | Security headers | Helmet | `app.use(helmet())` |
      | Rate limiting | express-rate-limit + Redis store | Stricter on auth endpoints |
      | CORS | cors package | Restrict to specific origins |
      | Dependency audit | `npm audit` | Run regularly in CI (see Dependency Supply Chain for caveats) |
      | Secrets | env vars via dotenv/vault | Validate at startup, never commit |
      
      ## Dependency Supply Chain
      
      `npm audit` catches *known advisories only* — not a freshly-malicious or typosquatted package. Harden the install itself:
      
      - **Frozen installs.** Commit the lockfile and install with the manager's immutable mode (`npm ci`, `pnpm install --frozen-lockfile`, `yarn install --immutable`) so CI can't silently resolve a different tree.
      - **Gate lifecycle scripts.** Block dependency `preinstall`/`postinstall` scripts by default and approve them per-package via the manager's native policy, so a compromised transitive dependency can't run arbitrary code at install time. The exact flag is manager- and version-specific — resolve it via Context7 rather than hardcoding it.
      - **Audit ≠ safety.** A clean `npm audit` is not proof a dependency is trustworthy. Never run `npm audit fix --force` unattended — it can jump majors and break the build. Treat audit as one signal, not a gate.
      - **Verify provenance** where the registry supports it (npm signature/provenance attestations) before adding a new or unfamiliar package.
      
      ## Secrets resolved by running a command
      
      A config option that fetches a secret by shelling out (`api_key_cmd`, git's `credential.helper`, AWS's `credential_process`, a `1password`/`gpg`/`vault` wrapper) is a subprocess whose failure modes are not the usual ones:
      
      - **A timeout on the child is not a timeout on the pipe.** Killing or cancelling the immediate child leaves a grandchild it spawned — `gpg-agent`, `pinentry`, a browser prompt — holding the inherited stdout descriptor, so the read never returns and the request hangs past its deadline. Set a hard timeout, then a short grace period after it, then force-close the output pipe rather than waiting on the reader. In Node this is `child_process.spawn` with `timeout` plus an explicit `killSignal`, followed by destroying `stdout`; the `timeout` option alone only signals the child.
      - **Cap the output buffer explicitly when using `spawn`.** An unbounded read of a credential helper's stdout means a misbehaving or wrong-binary command (a helper that prints a log, or a path that resolves to something that streams) grows the parent's heap until it dies. `spawn` has **no** `maxBuffer` option -- passing one is silently ignored, so count bytes in the `data` handler and kill the child and destroy the stream past the cap, reusing the `killSignal` from the timeout path above. `execFile` does honor `maxBuffer`, but it truncates the captured buffer to the cap and *then* reports `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` -- so a caller that ignores the error reads a silently shortened secret.
      
      Validate the resolved value for control bytes before it is used, per the CRLF/header-injection rows in `ia-code-review`'s security-patterns reference — a credential provider is an unusually direct path from external state into an `Authorization` header.
      
    • typescript-config.md 2.9 KB
      # TypeScript Configuration & Patterns
      
      > When to read: when setting up tsconfig, picking strictness flags, or choosing between branded types / discriminated unions / utility types for a backend service.
      
      ## Configuration
      
      tsconfig essentials:
      ```json
      {
        "compilerOptions": {
          "target": "ES2022",
          "module": "NodeNext",
          "moduleResolution": "NodeNext",
          "strict": true,
          "noUncheckedIndexedAccess": true,
          "exactOptionalPropertyTypes": true,
          "isolatedModules": true,
          "skipLibCheck": true,
          "outDir": "./dist",
          "rootDir": "./src"
        }
      }
      ```
      
      ESM-first: set `"type": "module"` in package.json.
      
      Dev: `tsx watch src/server.ts` | Build: `tsc` | Node 22.18+/23.6+: type stripping on by default; Node 22.6-22.17/23.0-23.5: `--experimental-strip-types` for scripts
      
      Type stripping only erases syntax, so a TypeScript construct with runtime semantics fails at Node startup instead of at build time: enum declarations, namespaces/modules carrying runtime code, constructor parameter properties, and non-ECMAScript `import =` / `export =` assignments are the confirmed offenders. Enable `erasableSyntaxOnly` in tsconfig to catch them at `tsc` type-check time instead of at `node` runtime.
      
      Type-safe env at startup -- Zod schema as source of truth:
      ```typescript
      import { z } from 'zod';
      const EnvSchema = z.object({
        PORT: z.coerce.number().default(3000),
        DATABASE_URL: z.string().url(),
        JWT_SECRET: z.string().min(32),
      });
      export type Env = z.infer<typeof EnvSchema>;
      export const env = EnvSchema.parse(process.env);
      ```
      
      ## Type Patterns
      
      **Branded types** -- prevent mixing domain primitives:
      ```typescript
      type Brand<K, T> = K & { __brand: T };
      type UserId = Brand<string, 'UserId'>;
      type OrderId = Brand<string, 'OrderId'>;
      // Compiler prevents passing OrderId where UserId expected
      ```
      
      **Discriminated unions** -- make illegal states unrepresentable:
      ```typescript
      type Result<T> = { ok: true; data: T } | { ok: false; error: string };
      ```
      
      **Exhaustive switch** -- catch missing cases at compile time:
      ```typescript
      default: { const _: never = status; throw new Error(`Unhandled: ${_}`); }
      ```
      
      **Type guards** for runtime narrowing:
      ```typescript
      function isAppError(err: unknown): err is AppError { return err instanceof AppError; }
      ```
      
      **`satisfies`** -- validate constraints, preserve literal types:
      ```typescript
      const config = { port: 3000, host: 'localhost' } satisfies Record<string, string | number>;
      ```
      
      **`as const`** -- literal unions from arrays:
      ```typescript
      const ROLES = ['admin', 'user', 'guest'] as const;
      type Role = typeof ROLES[number]; // 'admin' | 'user' | 'guest'
      ```
      
      ## Compiler Performance
      
      - `incremental: true` -- 50-90% faster rebuilds
      - `skipLibCheck: true` -- skip .d.ts checking
      - `isolatedModules: true` -- enables fast single-file transpilation
      - Avoid deeply nested generics and large unions (>100 members)
      - Diagnose: `npx tsc --extendedDiagnostics`
      
  • SKILL.md 4.1 KB
    ---
    name: ia-nodejs-backend
    class: language
    description: >-
      Node.js backend patterns: layered architecture, TypeScript, validation, error
      handling, security, observability, logging, metrics, deployment. Use when building REST APIs, REST endpoints, middleware,
      Express/Fastify/Hono/NestJS/Koa servers, tRPC procedures, Bun servers, or server-side TypeScript.
    paths: "**/*.ts,**/*.js,**/*.mjs,**/*.cjs"
    ---
    
    # Node.js Backend
    
    **Verify before implementing**: For framework-specific APIs (Express 5, Fastify 5, Node.js 22+ built-ins), look up current docs via Context7 (`query-docs`) before writing code. Training data may lag current releases.
    
    ## Working rules
    
    - Validate request and third-party data before use; keep response serialization and error envelopes explicit.
    - Preserve caller-visible contracts and authorization when adding resilience or fallbacks.
    - Bound concurrency, set timeouts, and avoid blocking production request paths.
    - Verify actual resource identity before parsing or caching a reused client's result.
    - Exercise operational telemetry and failure paths, not successful return codes alone.
    
    ## Architecture
    
    ```
    src/
    ├── routes/          # HTTP: parse request, call service, format response
    ├── middleware/       # Auth, validation, rate limiting, logging
    ├── services/        # Business logic (no HTTP types)
    ├── repositories/    # Data access only (queries, ORM)
    ├── config/          # Env, DB pool, constants
    └── types/           # Shared TypeScript interfaces
    ```
    
    - Routes never contain business logic
    - Services never import Request/Response
    - Repositories never throw HTTP errors
    - Dependencies point inward only (Clean Architecture rule): routes -> services -> repositories. Never the reverse.
    - For scripts/prototypes: single file is fine -- ask "will this grow?"
    
    
    ## TypeScript Rules
    
    - Use `import type { }` for type-only imports -- eliminates runtime overhead
    - Prefer `interface` for object shapes (2-5x faster type resolution than intersections)
    - Prefer `unknown` over `any` -- forces explicit narrowing
    - Use `z.infer<typeof Schema>` as single source of truth -- never duplicate types and schemas
    - Minimize `as` assertions -- use type guards instead
    - Add explicit return types to exported functions (faster declaration emit)
    - Untyped package? `declare module 'pkg' { const v: unknown; export default v; }` in `types/ambient.d.ts`
    
    
    ## Discipline
    
    - Simplicity first -- every change as simple as possible, impact minimal code
    - Only touch what's necessary -- avoid introducing unrelated changes
    - No hacky workarounds -- if a fix feels wrong, step back and implement the clean solution
    - Before adding a new abstraction, verify it appears in 3+ places. If not, inline it.
    - If a fix requires bypassing TypeScript (`as any`, non-null assertions on untrusted data, `// @ts-ignore`), treat it as a design smell and find the typed solution
    
    
    ## Verify
    
    - `tsc --noEmit` passes with zero errors
    - `npm test` passes with zero failures
    - No TypeScript bypasses (`as any`, `@ts-ignore`) in new code
    
    
    ## References
    
    - [TypeScript config](./references/typescript-config.md) -- tsconfig, ESM, branded types, compiler performance
    - [Security](./references/security.md) -- JWT, password hashing, rate limiting, OWASP
    - [API design patterns](./references/api-design.md) -- pagination, filtering, sorting, deprecation, idempotency-key claim and retention
    - [Database & production](./references/database-production.md) -- connection pooling, transactions, Docker, logging
    
    ## Task-specific references
    
    Read the relevant reference before implementing or reviewing the matching behavior:
    
    - For framework choice, input validation, API contracts, or errors: [api-boundaries.md](./references/api-boundaries.md).
    - For concurrency, networking, startup, caches, lifecycle cleanup, or telemetry: [async-and-production.md](./references/async-and-production.md).
    - For span kinds, HTTP-status-to-span-status rules, sampling placement, metric cardinality, or telemetry data governance: [observability-tracing.md](./references/observability-tracing.md).
    
    Existing specialized references, when the corresponding topic applies:
    
  • SPEC.md 4.3 KB
    # ia-nodejs-backend Specification
    
    ## Intent
    
    `ia-nodejs-backend` is a `language`-class skill (stack-specific patterns and idioms). Node.js backend patterns: layered architecture, TypeScript, validation, error handling, security, deployment. Use when building REST APIs, Express/Fastify/Hono/NestJS servers, or server-side TypeScript.
    
    ## Scope
    
    In scope:
    - Behaviors described in `SKILL.md` and routed via the should_trigger phrasings in `distillery/tests/fixtures/triggers/ia-nodejs-backend.jsonl`.
    - Updates to runtime behavior, structure, trigger precision, references, and validation.
    
    Out of scope:
    - Acting as the runtime instructions themselves (those live in `SKILL.md`).
    - Trigger phrasings already covered by adjacent `ia-*` skills (`validate-plugin` flags >70% description overlap as DUPLICATE_TRIGGER).
    - <!-- to fill in: domain-specific exclusions when the skill drifts -->
    
    ## Trigger Context
    
    - Class: `language`
    - Hook regex: `plugins/whetstone/hooks/skill-patterns.sh` -> `SKILL_PATTERNS[ia-nodejs-backend]`
    - Common requests (from fixture should_trigger):
      - "set up an Express server with middleware"
      - "build a Fastify API endpoint"
      - "write a Node.js backend API service"
    - Should not trigger for (from fixture should_not_trigger):
      - "write a Laravel controller for orders"
      - "create a React component for filters"
      - "write a Python script for ETL"
    
    ## Source And Evidence Model
    
    Authoritative sources:
    
    - `SKILL.md` -- runtime instructions and reference routing.
    - `references/*.md` -- bundled supplementary content (4 file(s)).
    - `distillery/tests/fixtures/triggers/ia-nodejs-backend.jsonl` -- positive and negative trigger phrasings under regression test.
    - `plugins/whetstone/hooks/skill-patterns.sh` -- regex pattern that fires this skill.
    - `distillery/.eval-data/ia-nodejs-backend/` -- harvested session examples (when present).
    
    Data that must not be stored in this skill or its references:
    
    - Secrets, credentials, tokens.
    - Machine-specific filesystem paths (`/home/...`, `/Users/...`, `~/ai/...`). The validator (`MACHINE_PATH_LEAK`) flags these as HIGH.
    - Private URLs, customer data, or unredacted personal information.
    
    ### Coverage matrix
    
    | Dimension | Status | Evidence |
    |---|---|---|
    | Trigger fixtures | complete | distillery/tests/fixtures/triggers/ia-nodejs-backend.jsonl (>=5 should_trigger, >=5 should_not_trigger) |
    | Hook regex pattern | complete | plugins/whetstone/hooks/skill-patterns.sh (`SKILL_PATTERNS[ia-nodejs-backend]`) |
    | Reference architecture | complete | 4 file(s) under references/ |
    | Real-usage signal | <!-- populated by harvest-sessions when sessions exist --> | distillery/.eval-data/ia-nodejs-backend/ (created by harvest-sessions) |
    
    ## Evaluation
    
    Lightweight (run on every change):
    
    ```bash
    python3 distillery/scripts/distiller.py validate-plugin --component ia-nodejs-backend
    python3 distillery/scripts/distiller.py test-triggers --skill ia-nodejs-backend
    ```
    
    Deeper (when behavior risk warrants):
    
    ```bash
    python3 distillery/scripts/distiller.py dspy-eval ia-nodejs-backend
    python3 distillery/scripts/distiller.py diagnose-negatives ia-nodejs-backend
    ```
    
    Acceptance gates:
    - `validate-plugin --component ia-nodejs-backend` returns 0 HIGH findings.
    - `test-triggers --skill ia-nodejs-backend` returns F1 = 1.0 with floors of 5 should_trigger and 5 should_not_trigger.
    - For dspy-eval, the composite score does not regress against the most recent saved baseline (see `distillery/.eval-data/ia-nodejs-backend/history.json`).
    
    ## Known Limitations
    
    <!-- to fill in over time as drift surfaces. Default rule: any time diagnose-negatives
         surfaces a recurring failure pattern, document it here so future maintainers
         understand the trade-off the current implementation accepts. -->
    
    ## Maintenance Notes
    
    - Update `SKILL.md` when the runtime workflow, branch conditions, or output contract changes.
    - Update this `SPEC.md` when intent, scope, evidence model, evaluation gates, or maintenance expectations change.
    - Update the trigger fixture when adding new positive phrasings, removing stale ones, or expanding scope (the 5/5 floor is a hard validator gate).
    - Update the hook regex in `skill-patterns.sh` whenever fixture positives expose a missed phrasing; verify F1 = 1.0 with `eval-triggers` before committing.
    - Run the full release pipeline via `/release` -- never bump versions or update CHANGELOG.md from a per-skill edit.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related