
Nodejs Backend
- 102 installs
- 31 repo stars
- Updated August 4, 2026
- iliaal/ai-skills
Node.js backend patterns covering layered architecture, TypeScript, validation, error handling, security, and framework selection for Express, Fastify, Hono, and NestJS.
About
The nodejs-backend skill provides patterns for building REST APIs, middleware, and server-side TypeScript across Express, Fastify, Hono, NestJS, and Bun. A developer uses it when building backend endpoints and wants framework selection guidance and layered architecture.
- Framework selection table by deployment target and needs
- Verify framework APIs via current docs before implementing
Nodejs Backend by the numbers
- 102 all-time installs (skills.sh)
- +7 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #2,978 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/ai-skills --skill nodejs-backendAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 102 |
|---|---|
| repo stars | ★ 31 |
| Last updated | August 4, 2026 |
| Repository | iliaal/ai-skills ↗ |
What it does
Node.js backend patterns covering layered architecture, TypeScript, validation, error handling, security, and framework selection for Express, Fastify, Hono, and NestJS.
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.
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); traces = "where did the time or the error go across services?".
- Structured logging:
pinowith a stable set of event names and a correlation/request ID propagated through async context (AsyncLocalStorage). Neverconsole.login production paths. - Metrics:
prom-clientfor 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,
/readyflapping), not on causes (CPU high, heap growing). A cause with no symptom is a dashboard, not a page.
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.