Node.js Best Practices
This page is a curated, opinionated checklist — a summary of the most impactful practices from across this entire guide. It's organized by area so you can use it as a pre-launch audit, an onboarding document, or a code-review reference. Each item links back to the page that covers it in depth. The goal isn't to follow every item blindly — it's to be deliberate: know the practice, understand why it matters, and when you skip it, do so consciously with a reason.
Project foundation
Use TypeScript from day one — strict mode, validate external data with Zod at every boundary.
Validate all config at startup — one typed config module, fail fast on missing vars,
Object.freezethe result.Set
NODE_ENV=productionin production — it matters for performance and security.Separate
app.tsfromserver.ts— make the app testable without binding a port.Commit a
.env.exampledocumenting all required variables; git-ignore.env.
Code quality and structure
Apply layered architecture — routes/controllers → services → repositories; each layer speaks only to the one below it.
Keep controllers thin — validate input, call a service, send the response; no business logic or SQL in route handlers.
Separate concerns — one reason to change per module; cross-cutting concerns go in middleware or injected infrastructure.
Use dependency injection — pass dependencies in, never import side-effectful modules directly; makes testing clean.
Throw typed domain errors — catch once at the boundary, map to HTTP in one central error middleware.
Automate style — ESLint + Prettier + Husky/lint-staged; style is not a code-review topic.
Async and performance
async/awaiteverywhere — callbacks only for legacy APIs wrapped withutil.promisify.Parallelize independent work with
Promise.all— sequentialawaitfor independent calls is a silent performance bug.Never block the event loop — no sync I/O, no CPU-heavy work on the main thread; use worker threads for CPU work.
Handle
uncaughtExceptionandunhandledRejection— log andprocess.exit(1); let the supervisor restart clean.Use cluster mode or multiple replicas — a single Node process uses one core; production uses all of them.
Security
Validate and sanitize every input — never trust client data; validate at the boundary.
Use Helmet — security headers on every response.
Rate-limit all endpoints — especially auth and public APIs.
Never log or leak secrets — redact credentials from logs, error responses, and traces.
Keep dependencies patched —
npm auditin CI; Dependabot/Renovate for automated PRs.Run as a non-root user in containers.
Observability
Structured JSON logging to stdout — levels, correlation IDs, no
console.login production.Health check endpoints — liveness (
/health/live) and readiness (/health/ready) with real dependency checks.Graceful shutdown — handle
SIGTERM, drain in-flight requests, close resources, then exit.Metrics and error tracking — know about problems before users report them.
Testing
Unit-test services with injected fakes — fast, no database, no network.
Integration-test routes with supertest + a real (test) database — verify the full request flow.
Co-locate tests (
*.test.tsbeside the module) — they stay visible and current.Aim for a testing pyramid — many unit tests, some integration, few E2E.
Run tests in CI — block merges on red.
Deployment and operations
Build once, deploy one artifact — same Docker image to staging and production; no rebuilding per environment.
Inject config at runtime — secrets from a secrets manager, not baked into the image.
Automate deploys via CI/CD — a green main branch deploys automatically; manual deploys are a reliability risk.
Use managed databases in production — let the cloud handle backups, HA, and patching.
Plan for horizontal scaling from the start — stateless processes, shared state in Redis/DB.