ADR-003: ES Modules (ESM)
Date: 2024-01-01 Status: Accepted
Context
Node.js supports both CommonJS (CJS) and ES Modules (ESM). We needed to choose which module system to use for this template, considering:
- Industry trends
- Node.js support
- Ecosystem compatibility
- TypeScript integration
- Future-proofing
Decision
We chose ES Modules (ESM) as the primary module system for all TypeScript code.
Rationale
- Future Standard: ESM is the standard module system for JavaScript (ECMAScript 2015+)
- Node.js Official Direction: Node.js moving towards ESM as primary
- Widespread Adoption: Most modern packages now support ESM
- Better Tooling: TypeScript and bundlers have excellent ESM support
- Async By Default: ESM is async-friendly, better for modern patterns
- Tree Shaking: Better dead code elimination with ESM
- Named Exports: More flexible export/import patterns
Consequences
Positive
- Future-proof, aligns with JavaScript standards
- Better interoperability with modern packages
- Smaller bundle sizes with better tree-shaking
- Clearer import/export semantics
- Better static analysis capabilities
Negative
- Some older packages still CommonJS-only
- Slightly more complex loading behavior
- Dynamic requires need workarounds
- Top-level await considerations
Configuration
{
"type": "module",
"compilerOptions": {
"module": "ESNext",
"target": "ES2022"
}
}
Alternatives Considered
- CommonJS: Mature, many packages support it, but deprecated in favor of ESM
- Mixed CJS/ESM: Complex, harder to maintain and debug
Migration Path
If reverting to CommonJS:
- Change
"type": "commonjs"inpackage.json - Update TypeScript config:
"module": "CommonJS" - Convert imports:
import→require - Convert exports:
export→module.exports - Update
package.jsonexports
Interoperability with CommonJS
For consuming CommonJS modules in ESM:
// Direct import of CJS module
import cjsModule from 'some-cjs-package';
// Using createRequire for edge cases
import { createRequire } from 'module';
const require = createRequire(import.meta.url);