Node.js Fundamentals Best Practices
A checklist for running Node.js services safely in production - version discipline, environment hygiene, and respect for the single-threaded model.
How to Use This List
- Walk top-to-bottom before every production deploy or LTS upgrade.
- Treat unchecked items as release blockers unless documented in an ADR exception.
- Revisit quarterly or when Node publishes a security release.
- Pair with section-specific lists (event loop, modules, TypeScript) for deeper audits.
A - Version & Runtime Discipline
- Run Active LTS (Node 24.18.0) or an approved Maintenance LTS window. Odd Current releases are dev-only.
- Pin
enginesinpackage.jsonand enforce in CI. Useengine-strict=trueor a preinstall gate script. - Commit
.nvmrcor.node-versionand match Docker base images.node:24.18.0-bookworm-slim, notnode:latest. - Track LTS end dates in the team calendar. Schedule upgrades 6 months before End-of-Life.
- Log
process.versionat startup and expose it in/health. Speeds incident triage when fleets drift.
B - Project & Module Hygiene
- Default new services to ESM (
"type": "module"). Document any CJS exception with an ADR. - Use
node:prefix for built-in imports.import fs from 'node:fs/promises'clarifies intent. - Prefer built-in
fetch,node:test, andnode:assertbefore npm equivalents. Fewer supply-chain surfaces. - Keep
package-lock.jsoncommitted and usenpm ciin CI. Reproducible installs across machines. - Audit native addons after every Node major bump. bcrypt, sharp, and similar must rebuild cleanly.
C - Configuration & Globals
- Validate environment variables at startup with a schema (Zod). Fail fast before accepting traffic.
- Never mutate
process.envafter boot except in tests. Treat env as immutable configuration. - Avoid storing request-scoped data on global variables. Use
AsyncLocalStoragefor request context. - Set
NODE_ENV=productionin deployed environments. Some libraries change behavior based on it. - Do not log secrets, tokens, or full
process.envdumps. Redact in structured logs.
D - Process & Operational Safety
- Handle
SIGTERMandSIGINTfor graceful shutdown. Close servers and DB pools before exit. - Treat unhandled rejections as fatal in production. Use
process.on('unhandledRejection')to log and exit. - Cap JSON body size at the reverse proxy and application layer. Protect the event loop from huge
JSON.parse. - Run with a non-root user in containers. Official Node images provide the
nodeuser. - Enable
--trace-warningsand--trace-deprecationin CI. Catch upgrade debt before production.
E - Observability & Documentation
- Bookmark official docs for your pinned major (v24.x). Do not rely on outdated blog posts.
- Capture Node warnings in logs during deploy canaries. Deprecations predict next-major breakage.
- Know how to generate a diagnostic report (
process.report). Practice before the first 3 a.m. page. - Document runtime ADRs when evaluating Bun/Deno. Default stays Node until evidence says otherwise.
- Keep runbooks linked from the repo README. Include version check and rollback commands.
FAQs
Why is pinning Node version the first item?
Version drift is the root cause of most "works locally" incidents - wrong APIs, failed native builds, and unmatched security patches.
Can we skip ESM if the team knows CJS?
New services should use ESM on Node 24. CJS is legacy maintenance - exceptions need an ADR and sunset date.
Is NODE_ENV still necessary with containers?
Many libraries (Express in production mode, some log formatters) still branch on it. Set explicitly - do not rely on absence.
What engines range should we use?
>=24.18.0 <25 for Node 24 Active LTS. During migration, document a temporary dual-range with an expiry date.
Should we fail on npm audit warnings?
Treat critical/high severities as blockers. Fundamentals list focuses on runtime; see Security section for full policy.
Why log process.version in health checks?
Orchestrators may restart pods with stale images. Health output proves the running binary matches the intended patch.
Are global variables ever acceptable?
Module-level singletons (DB pool, config) are fine. Per-request data on globals is not - use AsyncLocalStorage.
How do we enforce engine-strict?
Add to .npmrc:
engine-strict=true
What about Deno or Bun in dev only?
Allowed if production stays pinned Node 24 and CI tests against the production runtime - not dev-only shortcuts.
How often should we re-run this checklist?
Every release candidate and within one week of any Node security advisory affecting your major line.
Does this replace event-loop best practices?
No. This list covers fundamentals; pair with Event Loop Best Practices for async performance.
What is the minimum health endpoint payload?
status, node version, and build/git SHA. Optional: ltsPolicy field from your version gate script.
Related
- Installing & Version Management - tooling to enforce versions
- Node.js Release & LTS Policy - LTS lifecycle detail
- How Node.js Works - why globals and blocking matter
- Event Loop Best Practices - async performance rules
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.