NodeJSNode.js Best Practices

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.freeze the result.

  • Set NODE_ENV=production in production — it matters for performance and security.

  • Separate app.ts from server.ts — make the app testable without binding a port.

  • Commit a .env.example documenting 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 styleESLint + Prettier + Husky/lint-staged; style is not a code-review topic.

Async and performance
  • async/await everywhere — callbacks only for legacy APIs wrapped with util.promisify.

  • Parallelize independent work with Promise.all — sequential await for 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 uncaughtException and unhandledRejection — log and process.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 patchednpm audit in 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.log in 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.ts beside 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.

These practices compound — each one independently helps, but together they define the difference between a demo and a production service
No single item here is revolutionary — each is a well-understood, widely-agreed practice. What's powerful is their **compounding effect**: TypeScript catches bugs ESLint misses, layered architecture makes DI natural, DI makes unit testing trivial, unit tests give you confidence to deploy automatically, automated deploys make each release small and low-risk, graceful shutdown makes each deploy safe, and health checks make each deploy verifiable. The teams that ship reliably aren't doing anything exotic — they've internalized a set of practices like these and apply them consistently. Use this list as a starting point, adapt it to your context, and revisit it as your app grows.
Next
Take your architecture to the next level: [Introduction to Microservices](/nodejs/microservices-intro).