nodejs-backend
Node.js backend patterns: framework selection, layered architecture, TypeScript, validation, error handling, security, production deployment. Use when building REST APIs, Express/Fastify servers, microservices, or server-side TypeScript.
Install
npx skills add https://github.com/iliaal/whetstone/tree/master/distillery/generated-skills/nodejs-backend
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install iliaal-whetstone@llmmart
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
Framework Selection
| Context | Choose | Why |
|---|---|---|
| Edge/Serverless | Hono | Zero-dep, fastest cold starts |
| Performance API | Fastify | 2-3x faster 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
- 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
- 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 } } - Errors:
{ error: { code, message, details? } } - 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 readFileSync / sync methods in production. Offload CPU work to worker threads. Stream large payloads.
Discipline
- For non-trivial changes, pause and ask: "is there a more elegant way?" Skip for obvious fixes.
- 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
References
- TypeScript config — tsconfig, ESM, branded types, compiler performance
- Security — JWT, password hashing, rate limiting, OWASP
- Database & production — connection pooling, transactions, Docker, logging
Files (whetstone)
-
references
-
database-production.md 1.2 KB
# Database & Production ## 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: ```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. ## 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 -
security.md 1.3 KB
# Authentication & Security ## Authentication - **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 ## Security Checklist - [ ] All inputs validated (Zod/TypeBox at route boundary) - [ ] Parameterized queries only (no string concatenation) - [ ] Passwords hashed (bcrypt/argon2, never plaintext) - [ ] JWT: verify signature + expiry, short-lived access tokens - [ ] Rate limiting (express-rate-limit + Redis store, stricter on auth endpoints) - [ ] Security headers (Helmet) - [ ] HTTPS everywhere in production - [ ] CORS restricted to specific origins - [ ] Secrets from env vars only, validated at startup - [ ] `npm audit` regularly - [ ] No stack traces in production error responses - [ ] Authorization per request, not just authentication OWASP API Top 10: Broken Object-Level Auth | Broken Auth | Broken Property-Level Auth | Unrestricted Resource Consumption | Broken Function-Level Auth | Sensitive Business Flow | SSRF | Security Misconfiguration | Improper Inventory | Unsafe API Consumption -
typescript-config.md 2.2 KB
# TypeScript Configuration & Patterns ## 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+: `--experimental-strip-types` for scripts 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`
-
-
manifest.json 1.2 KB
{ "query": "nodejs-backend", "search_queries": [ "nodejs", "javascript backend", "typescript" ], "instructions": "Exclude front-end related patterns & concepts. Include TypeScript.", "generated": "2026-02-13", "token_count": 1161, "sources": [ { "id": "wshobson/agents/typescript-advanced-types", "installs": 4473, "sha1": "dc08d7b95f19533cf758739162f0fd82a5217f39" }, { "id": "wshobson/agents/nodejs-backend-patterns", "installs": 3541, "sha1": "b9aed50c84f7253e3805c33ab8556904c3e45a48" }, { "id": "sickn33/antigravity-awesome-skills/typescript-expert", "installs": 1050, "sha1": "a85100f5e5fb7b03bfdbbb06095b29602da45cd5" }, { "id": "sickn33/antigravity-awesome-skills/api-security-best-practices", "installs": 815, "sha1": "68e4f51e2702a060146107ee64e51059fe2b2fda" }, { "id": "sickn33/antigravity-awesome-skills/nodejs-best-practices", "installs": 521, "sha1": "26900cb9792a83bd4c846c96b9083b93fc23c60d" }, { "id": "pproenca/dot-skills/typescript", "installs": 480, "sha1": "5ce69fa74cdae95a345d61b28eec32e68b9cc341" } ] } -
SKILL.md 4.3 KB
--- name: nodejs-backend description: >- Node.js backend patterns: framework selection, layered architecture, TypeScript, validation, error handling, security, production deployment. Use when building REST APIs, Express/Fastify servers, microservices, or server-side TypeScript. --- # Node.js Backend ## Framework Selection | Context | Choose | Why | |---------|--------|-----| | Edge/Serverless | Hono | Zero-dep, fastest cold starts | | Performance API | Fastify | 2-3x faster 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 - 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` ## 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 - **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 } }` - **Errors**: `{ error: { code, message, details? } }` - **Queries**: `?page=1&limit=20&status=active&sort=createdAt,desc` - Return `Location` header 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 `readFileSync` / sync methods in production. Offload CPU work to worker threads. Stream large payloads. ## Discipline - For non-trivial changes, pause and ask: "is there a more elegant way?" Skip for obvious fixes. - 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 ## References - [TypeScript config](references/typescript-config.md) — tsconfig, ESM, branded types, compiler performance - [Security](references/security.md) — JWT, password hashing, rate limiting, OWASP - [Database & production](references/database-production.md) — connection pooling, transactions, Docker, logging
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.