Org Skills

nodejs-development

Build production-grade Node.js back-end services in TypeScript — framework choice (Express, Fastify, NestJS, Hono), strict tsconfig and ESM, schema validation at the edge, central error handling, validated config, data access and transactions, resilient outbound HTTP, auth, pino logging, graceful shutdown, background jobs and security basics.

Download .zip Raw Source
When agents use itUse this whenever the user asks to write, scaffold, refactor or debug a Node/TypeScript API, route, controller, middleware or worker; edits package.json, tsconfig.json, server.ts, app.ts, main.ts, app.module.ts or .env; asks about Zod, TypeBox, Prisma, Drizzle, Knex, TypeORM, JWT, sessions, OAuth, BullMQ, helmet, rate limiting, OpenAPI, "unhandled promise rejection", "ERR_REQUIRE_ESM" or "how do I do X in Express/Fastify/NestJS". Also apply it when reviewing Node.js back-end code or pull requests, even if the question is about a single handler.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill nodejs-development -a github-copilot
# or with the org installer (adds .github/skills/nodejs-development):
npx -y github:AGCO-Global/org-skills add skill nodejs-development

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill nodejs-development
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill nodejs-development -g

Installs the backend-nodejs-skills plugin, which bundles all Backend / Node.js skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install backend-nodejs-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill nodejs-development -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill nodejs-development -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill nodejs-development -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill nodejs-development -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

Node.js Development

A Node service is one process multiplexing thousands of requests on a single thread: anything that blocks, leaks, throws unhandled or trusts its input hurts every request at once. Write each handler assuming hostile input, a slow dependency and a SIGTERM arriving mid-request. Target the active Node LTS unless the project pins a version (engines, .nvmrc, .node-version, Dockerfile base image).

1. Pick the framework deliberately

Framework Default when Watch out for
Fastify New HTTP/JSON APIs; you want schema-first validation, fast serialization, plugin encapsulation Plugin scoping (fastify-plugin) confuses newcomers
NestJS Large teams, many modules, want opinionated DI, guards, interceptors, OpenAPI generation Decorator/reflect-metadata magic, heavier startup; can run on Fastify adapter
Express 5 Existing Express codebases, huge middleware ecosystem, simple services Slower, no built-in validation or schemas; Express 5 forwards rejected promises, v4 does not
Hono Edge/serverless or multi-runtime (Node, Bun, Workers, Deno), small footprint Smaller ecosystem for server-only concerns (sessions, queues)

Don't migrate frameworks for fashion. For greenfield: Fastify for a lean service, NestJS when team size and module count justify the structure.

2. TypeScript and module setup

// tsconfig.json (build); pair with "type": "module" in package.json
{ "compilerOptions": {
    "target": "ES2023", "module": "NodeNext", "moduleResolution": "NodeNext",
    "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true,
    "verbatimModuleSyntax": true, "isolatedModules": true,
    "outDir": "dist", "sourceMap": true, "skipLibCheck": true } }
  • ESM for new projects ("type": "module", relative imports with .js extensions under NodeNext). Stay CommonJS only when a key dependency or tooling (some NestJS setups, older Jest) forces it; don't mix both in one package. Recent Node versions can require() synchronous ESM, but don't design around it.
  • Dev loop with tsx (or Node's built-in type stripping on recent versions, which only handles erasable syntax); production runs compiled JS from dist/. tsc --noEmit in CI regardless.
  • Layout: src/modules/<feature>/{routes,service,repository,schemas}.ts, src/config.ts, src/app.ts (builds the app, no listen), src/server.ts (listens, handles signals). Separating app from server is what makes in-process testing possible.

3. Validate at the edge, trust inside

  • Every request body, query, params and header you read is parsed by a schema: TypeBox or Zod with Fastify (type provider), Zod with Express/Hono, class-validator + ValidationPipe (whitelist: true, forbidNonWhitelisted: true) or Zod pipes in NestJS.
  • Infer TS types from the schema; never hand-write a parallel interface.
  • Validate responses too (Fastify response schemas also strip unlisted fields, preventing accidental leaks like passwordHash).
const CreateOrder = z.object({ sku: z.string().min(1), qty: z.number().int().positive().max(100) });
app.post('/orders', async (req, res) => {
  const input = CreateOrder.parse(req.body);        // throws ZodError → error handler → 400
  res.status(201).json(await orders.create(input, req.user.id));
});

4. Errors: operational vs programmer

  • Operational (bad input, not found, conflict, dependency timeout): throw typed domain errors (NotFoundError, ConflictError) with a code; one central handler maps them to HTTP status and a stable body (RFC 9457 problem+json is a good default). Never leak stack traces or SQL to clients.
  • Programmer errors (TypeError, invariant broken): log with full context, return 500, and treat as a bug.
  • Async errors must reach the handler: Fastify and Express 5 handle rejected async handlers; in Express 4 wrap handlers or upgrade. Always await or return promises; floating promises are banned (@typescript-eslint/no-floating-promises).
  • Central handler: Fastify setErrorHandler, Express 4-arg middleware registered last, NestJS exception filters.
  • Process level: on uncaughtException / unhandledRejection, log fatally and exit non-zero; let the orchestrator restart. Continuing after an uncaught exception runs in an unknown state.

5. Config and secrets

// src/config.ts: the only file that reads process.env
const Env = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_ISSUER: z.string().url(),
});
export const config = Env.parse(process.env);     // fail fast at boot with a clear message
  • Inject config (or slices of it); process.env.X scattered through the code is untestable and unvalidated.
  • .env only for local dev (node --env-file=.env or dotenv), git-ignored, with a committed .env.example. Production secrets come from the platform's secret manager, mounted as env or files; never baked into images or logged.

6. Data access

Tool Choose when Trade-off
Prisma Productivity, schema-first migrations, typed client Less SQL control; check generated queries; $transaction for multi-step writes
Drizzle SQL-shaped, lightweight, typed; serverless-friendly Younger ecosystem, you design more yourself
Knex / Kysely Query builder, full SQL control (Kysely for types) You own mapping and migrations discipline
TypeORM / MikroORM Team wants Unit of Work/entity patterns (common with NestJS) Lazy-loading and N+1 surprises; review generated SQL
  • Parameterized queries only; raw SQL through tagged templates (sql`...`, Prisma.sql), never string concatenation.
  • One pool per process sized to DB max_connections / replicas; in serverless use a proxy/pooler (PgBouncer, RDS Proxy, driver adapters).
  • Transactions around every multi-write invariant; keep them short, no outbound HTTP inside a transaction.
  • Repositories own queries; services never build SQL. Migrations are versioned files run in CI/CD, not synchronize: true.

7. Outbound HTTP

  • Use global fetch (undici) or undici.request; every call has a timeout: fetch(url, { signal: AbortSignal.timeout(2000) }). Combine with the request's own abort via AbortSignal.any([...]).
  • Retry only idempotent operations, on 5xx/429/network errors, with exponential backoff + jitter and a cap; honour Retry-After. Send idempotency keys for retried POSTs.
  • Wrap each dependency in a typed client module; validate its responses with a schema. Add a circuit breaker (e.g. opossum) for flaky dependencies on hot paths.

8. Auth

  • Don't roll crypto: argon2/bcrypt for passwords, jose for JWT, established OAuth/OIDC libraries or a managed IdP.
  • JWT: verify signature, alg allow-list, iss, aud, exp; short-lived access tokens, rotated refresh tokens. Sessions (httpOnly, Secure, SameSite cookies, server-side store like Redis) are simpler and revocable for first-party web apps.
  • Authorize per resource in the service layer ("can this user act on order 42?"), not only per route.

9. Logging, shutdown, jobs, security

  • pino (Fastify's built-in logger; nestjs-pino/pino-http elsewhere): JSON logs, request id on every line via child loggers, redact: ['req.headers.authorization', 'req.headers.cookie', '*.password']. No console.log in services.
  • Graceful shutdown on SIGTERM/SIGINT: fail readiness, stop accepting connections (server.close() / app.close()), drain in-flight requests and workers, close DB/Redis/queues, then exit; force-exit after a timeout shorter than the orchestrator's grace period.
  • Background jobs with BullMQ (Redis) or a DB-backed queue (pg-boss, Graphile Worker) in a separate worker process; jobs are idempotent, have retries with backoff and a dead-letter path. Never do slow work after sending the HTTP response in the same process "fire and forget".
  • Security: helmet/@fastify/helmet, CORS allow-list, rate limiting (@fastify/rate-limit, express-rate-limit, backed by Redis when multi-instance), body size limits, npm audit/Dependabot/Renovate in CI, lockfile committed, npm ci in builds, run as non-root.
  • OpenAPI: generate from the same schemas (@fastify/swagger, @nestjs/swagger, zod-to-openapi) and publish it; contract drift is a bug.

Deliverable format

When writing or reviewing a Node service change, output:

## Summary        what changed and why (1–3 lines)
## Code           files with full, compiling TypeScript (schemas, handler, service, repository)
## Errors         which domain errors are thrown and their HTTP mapping
## Config         new env vars (added to config schema and .env.example)
## Tests          unit + in-process HTTP tests added
## Risks          migrations, breaking API changes, rollout notes

Anti-patterns to reject

  • process.env.X read outside the config module; unvalidated or missing-default config.
  • req.body used without a schema; hand-written types duplicating schemas; any on request data.
  • Floating promises, async Express 4 handlers without error forwarding, empty catch, catch (e) { console.log(e) }.
  • Keeping the process alive after uncaughtException.
  • SQL built by string concatenation; synchronize: true in production; a new pool per request.
  • fetch without a timeout; retrying non-idempotent calls blindly.
  • Custom JWT parsing, jwt.decode instead of verify, secrets committed or logged.
  • server.listen inside app.ts, making the app untestable in-process.
  • Sync APIs (fs.readFileSync, crypto.pbkdf2Sync) on request paths.