Modules Best Practices
Module boundaries shape deployability and refactors - standardize on ESM, explicit exports, and honest package.json dependencies unless an ADR documents a dual-module exception.
Search across all documentation pages
Module boundaries shape deployability and refactors - standardize on ESM, explicit exports, and honest package.json dependencies unless an ADR documents a dual-module exception.
"type": "module" on new application packages. CJS only for .cjs configs and legacy shims..mjs / .cjs extensions when mixing systems in one package. Never rely on ambiguous .js parsing.node: prefix on built-in imports. Avoid shadowing and clarify intent."exports" for libraries and shared monorepo packages. Block deep imports into src/.dependencies. Do not rely on hoisted phantom deps.exports at compiled dist/, not TypeScript source. Consumers run JS matching Node resolution."./package.json" only when tooling requires it. Keep surface minimal otherwise.workspace:*) consistently in monorepos. Lock versions on publish.import type for type-only imports. Enables verbatimModuleSyntax and cleaner bundles..js extensions in relative ESM import paths. Matches NodeNext emit and runtime.import() to plugins and optional code paths. Keep core graph static and analyzable."module": "NodeNext" and "moduleResolution": "NodeNext". Matches Node 24 ESM resolver.tsconfig paths fix runtime. Emit correct paths or use build tooling.verbatimModuleSyntax for new projects. Separates type imports explicitly.node dist/main.js in CI, not only tsx src. Catches extension and exports mistakes..d.ts next to emitted JS with matching exports types condition.npm pack --dry-run before publishing. Verify files and exports include only intended artifacts.import and require if dual publishing. Prevent dual package hazard regressions.#internal import maps for package-private modules. Never expose # paths via exports.ERR_PACKAGE_PATH_NOT_EXPORTED in consumer apps after tightening exports. Communicate breaking changes.No - applications deploy one module system. Dual publishing is for libraries with external CJS consumers.
Phantom dependencies from hoisting - import works until a clean npm ci in Docker.
CLI entry with single function, or framework convention - not for shared domain libraries.
Rarely at runtime - prefer exports subpaths for public API. Aliases are OK inside apps if build enforces them.
ESLint import/extensions or TypeScript moduleResolution: NodeNext compile errors.
Yes with modern compiler settings - verify third-party Nest modules support ESM.
readFile + Zod validation is explicit - or JSON import attributes per Node 24 docs.
No - export curated public API only. Large barrels harm compile times and circular deps.
Leaf utilities first, createRequire bridges temporarily, .cjs only for unmigrated edges.
Same principles - pnpm is stricter about declared deps, which helps enforce this list.
Knip, dependency-cruiser, and npm ls <pkg> in CI for undeclared imports.
CJS ↔ ESM Interop for bridges and dual-package hazards.
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.
Reviewed by Chris St. John·Last updated Jul 19, 2026