
Ia Nodejs Backend
- 3 installs
- 28 repo stars
- Updated August 5, 2026
- iliaal/whetstone
Guides building Node.js REST APIs with layered architecture, TypeScript, Zod validation, error hierarchies, and production resilience patterns.
About
A skill covering Node.js backend patterns including framework selection, clean layered architecture, TypeScript rules, boundary validation, and production resilience. A developer uses it when building REST APIs or server-side TypeScript with Express, Fastify, Hono, or NestJS.
- Framework selection table plus routes/services/repositories architecture
- Custom error hierarchy, health/ready endpoints, and circuit breakers
Ia Nodejs Backend by the numbers
- 3 all-time installs (skills.sh)
- Ranked #3,739 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/iliaal/whetstone --skill ia-nodejs-backendAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 28 |
| Last updated | August 5, 2026 |
| Repository | iliaal/whetstone ↗ |
What it does
Guides building Node.js REST APIs with layered architecture, TypeScript, Zod validation, error hierarchies, and production resilience patterns.
Files
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.
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.
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
interfacefor object shapes (2-5x faster type resolution than intersections) - Prefer
unknownoverany-- forces explicit narrowing - Use
z.infer<typeof Schema>as single source of truth -- never duplicate types and schemas - Minimize
asassertions -- 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; }intypes/ambient.d.ts
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.
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
Locationheader on 201. Use 204 for successful DELETE with no body.
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/readyfor traffic routing; monitoring probes/healthfor 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-stringifyfor 2-3x faster serialization. - Circuit breaker: use
opossumfor outbound service calls. States: CLOSED (normal) -> OPEN (failing, return fallback) -> HALF_OPEN (probe). Prevents cascade failures when downstream services are down.
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 && npm testpass with zero warnings before declaring done
Verify
tsc --noEmitpasses with zero errorsnpm testpasses with zero failures- No TypeScript bypasses (
as any,@ts-ignore) in new code
References
- TypeScript config -- tsconfig, ESM, branded types, compiler performance
- Security -- JWT, password hashing, rate limiting, OWASP
- API design patterns -- pagination, filtering, sorting, deprecation
- Database & production -- connection pooling, transactions, Docker, logging
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:
SELECT * FROM items
WHERE id > :cursor_id
ORDER BY id ASC
LIMIT :limit + 1; -- fetch N+1 to determine has_nextResponse: { 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 fieldsComma-separated for multi-value equality:
?category=electronics,clothingSorting
Prefix - for descending, comma-separated for multi-field:
?sort=-created_at,name # newest first, then alphabeticalSparse Fieldsets
?fields=id,name,email # return only these fieldsDeprecation 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)
- [ ] External API responses validated before use
- [ ] OpenAPI/docs updated
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()
Index strategies:
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); -- coveringAlways 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-postgresqlskill 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-alpinebuilder + prod image withnpm 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 /healthreturning{ status: 'ok' } - Compression: gzip/brotli via middleware
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
Bearertoken →jwt.verify→ attachreq.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 |
| Secrets | env vars via dotenv/vault | Validate at startup, never commit |
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:
{
"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+: --experimental-strip-types for scripts
Type-safe env at startup -- Zod schema as source of truth:
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:
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
// Compiler prevents passing OrderId where UserId expectedDiscriminated unions -- make illegal states unrepresentable:
type Result<T> = { ok: true; data: T } | { ok: false; error: string };Exhaustive switch -- catch missing cases at compile time:
default: { const _: never = status; throw new Error(`Unhandled: ${_}`); }Type guards for runtime narrowing:
function isAppError(err: unknown): err is AppError { return err instanceof AppError; }`satisfies` -- validate constraints, preserve literal types:
const config = { port: 3000, host: 'localhost' } satisfies Record<string, string | number>;`as const` -- literal unions from arrays:
const ROLES = ['admin', 'user', 'guest'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'user' | 'guest'Compiler Performance
incremental: true-- 50-90% faster rebuildsskipLibCheck: true-- skip .d.ts checkingisolatedModules: true-- enables fast single-file transpilation- Avoid deeply nested generics and large unions (>100 members)
- Diagnose:
npx tsc --extendedDiagnostics
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.mdand routed via the should_trigger phrasings indistillery/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-pluginflags >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):
python3 distillery/scripts/distiller.py validate-plugin --component ia-nodejs-backend
python3 distillery/scripts/distiller.py test-triggers --skill ia-nodejs-backendDeeper (when behavior risk warrants):
python3 distillery/scripts/distiller.py dspy-eval ia-nodejs-backend
python3 distillery/scripts/distiller.py diagnose-negatives ia-nodejs-backendAcceptance gates:
validate-plugin --component ia-nodejs-backendreturns 0 HIGH findings.test-triggers --skill ia-nodejs-backendreturns 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.mdwhen the runtime workflow, branch conditions, or output contract changes. - Update this
SPEC.mdwhen 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.shwhenever fixture positives expose a missed phrasing; verify F1 = 1.0 witheval-triggersbefore committing. - Run the full release pipeline via
/release-- never bump versions or update CHANGELOG.md from a per-skill edit.