Node.js has made server-side JavaScript a breeze, but the way we bring code into a file—modules—has evolved over time. While CommonJS and its require()
syntax served us well, ES Modules (import
/export
) are the modern standard. Yet many developers still mix both styles or stick to require()
out of habit. Ever wondered why mixing require()
and import
sometimes leads to odd errors or broken builds?
Switching fully to ES Modules and import
solves those headaches. Understanding how to enable ESM in Node.js, migrate existing code, and handle interoperability lets you write cleaner, future-proof code. Let’s dive into why import
matters and how it benefits your next project.
Why ESM Matters
ES Modules (ESM) are the official JavaScript module standard. Browsers and modern build tools embrace them, giving you:
- Static analysis: Tools can tree-shake unused exports at build time.
-
Cleaner syntax:
import
statements sit at the top and clearly show dependencies. - Future compatibility: Align your server code with browser code.
Node.js supported CommonJS first, but recent releases back ESM natively. By declaring modules as ESM, you avoid runtime flags and Babel transpilation. You also tap into features like import.meta.url
and top-level await
. If you haven’t already, read about Node.js’s event loop and Node.js asynchronous nature to see how modules load in a non-blocking system.
Tip: Pick one module system per project. Mixing can lead to confusing loader errors.
Enable ES Modules
To run ESM in Node.js:
- In your
package.json
, set:
{
"type": "module"
}
- Rename files to
.mjs
or keep.js
iftype
is set. - Use
node file.js
as usual.
If you need compatibility, you can also:
- Use the
--experimental-modules
flag on older Node versions. - Mix
.cjs
and.mjs
extensions to separate CommonJS and ESM files.
For a quick start, see this Node.js project setup guide in VS Code.
Migrate Your Code
When moving an existing codebase:
- Update imports:
// Old CommonJS
const fs = require('fs');
// New ESM
import fs from 'fs';
-
Handle default vs named exports:
- CommonJS modules often export an object. In ESM, you may need
import pkg from 'pkg'
. - For named exports, use
export const x = 1;
thenimport { x } from './file.js'
.
- CommonJS modules often export an object. In ESM, you may need
-
Check third-party libraries: Some packages still ship only CommonJS. You can:
- Import using dynamic
import()
. - Keep certain files as
.cjs
and load withrequire()
.
- Import using dynamic
Tip: Run your test suite after each batch of changes to catch missing exports early.
Interop Between Worlds
Sometimes you need both systems. Here’s a quick comparison:
Feature | CommonJS (require ) |
ES Modules (import ) |
---|---|---|
Syntax location | Inline | Top of file |
Dynamic loading |
require() works anywhere |
Use import() function |
Named imports | Destructure after load | Native named imports |
File extension support |
.js , .cjs
|
.mjs , .js with config |
Dynamic import()
lets you load modules at runtime:
async function loadConfig() {
const config = await import('./config.js');
return config.default;
}
Quote: “Interop is possible, but clarity wins. Strive to convert entire modules when you can.”
Performance Tips
Switching to import
rarely affects raw performance. But static import
lets build tools optimize your bundle:
- Tree-shaking: Dead code elimination with Rollup or webpack.
- Faster startup: Less parsing overhead if you only import what you need.
Local tips:
- Group related imports to help bundlers:
import { readFile, writeFile } from 'fs/promises';
- Use top-level
await
in ESM for sequential startup tasks:
const config = await import('./config.js');
Tip: Benchmark critical paths using simple timers before and after migration.
Best Practices
- Always prefer named exports where possible. It makes code self-documenting.
- Keep your
package.json
type
field consistent across services. - Automate migrations with codemods or simple regex scripts.
- Lint for mixed syntax using ESLint rules
no-commonjs
or plugins.
Conclusion
ES Modules in Node.js are no longer experimental—they’re the future of modular JavaScript. Moving from require()
to import
helps you align with browser code, enable advanced build optimizations, and simplify dependency management. By updating your package.json
, renaming extensions, and migrating exports step by step, you can avoid downtime and buggy behavior. Remember to test thoroughly and keep an eye on interoperability when third-party packages haven’t caught up.
Takeaway: Commit to one module system per project, lean on static import
for clarity, and embrace the modern JavaScript ecosystem with confidence.
Top comments (0)