Node.js ESM vs CommonJS: Which Module System Should You Use
Problem
I was setting up a new Node.js project and got stuck on a simple question: should I use ESM or CommonJS? Every tutorial seemed to have a different answer. Some said CommonJS is still the default, others said ESM is the future. I needed a clear answer.
That led me down a rabbit hole of module system history, and here’s what I found.
The Short Answer
Use ESM for all new projects in 2025. CommonJS should only be used for maintaining legacy codebases that cannot be migrated.
What Are These Module Systems?
CommonJS (CJS)
CommonJS has been the default in Node.js since the beginning. It uses require() to import and module.exports to export:
// CommonJSconst fs = require('fs');const { parse } = require('path');
module.exports = { readFile: (filePath) => fs.readFileSync(filePath, 'utf-8'), getBasename: (filePath) => parse(filePath).base};ES Modules (ESM)
ESM is the standardized JavaScript module system. It uses import and export:
// ES Modulesimport fs from 'fs';import { parse } from 'path';
export const readFile = (filePath) => fs.readFileSync(filePath, 'utf-8');export const getBasename = (filePath) => parse(filePath).base;Why ESM Is the Default Choice Now
1. It’s the JavaScript Standard
ESM is part of the ECMAScript standard, meaning it works the same in browsers, Node.js, and other JavaScript environments. CommonJS is a Node.js-specific solution that never gained traction outside the server.
When you write ESM, your code is more portable. The same imports work in a frontend React app, a Node.js backend, and a serverless function.
2. Better Tree Shaking
Bundlers like webpack, Rollup, and Vite can analyze ESM imports at build time and remove unused code. This leads to smaller bundle sizes and better performance:
export const add = (a, b) => a + b;export const subtract = (a, b) => a - b;export const multiply = (a, b) => a * b;If you only import add, bundlers will exclude subtract and multiply from your production bundle. CommonJS makes this much harder because require() is dynamic and harder to analyze statically.
3. Top-Level Await
ESM supports top-level await, meaning you can use await outside of async functions:
// ESM - works!const config = await fetch('/config.json').then(r => r.json());export default config;This is incredibly useful for loading configuration, initializing connections, and other startup tasks without wrapping everything in async IIFEs.
4. Named Exports Are First-Class
ESM encourages named exports, which leads to more explicit and self-documenting code:
// ESM - explicit about what you're gettingimport { get, post, put, delete } from './http-client.js';Compare to CommonJS where you might do:
// CommonJS - what's actually exported?const client = require('./http-client');module.exports = client;5. Native Browser Support
ESM works natively in browsers without bundlers:
<script type="module" src="app.js"></script>If you’re building any kind of full-stack JavaScript application, using ESM everywhere means you can share code between client and server.
How to Use ESM in Node.js
Option 1: Use .mjs Extension
The simplest way to opt into ESM is to use the .mjs file extension:
import { readFile } from 'fs/promises';
const data = await readFile('./package.json', 'utf-8');console.log(JSON.parse(data).name);Option 2: Add “type”: “module” to package.json
This makes all .js files in your project use ESM:
{ "name": "my-project", "type": "module", "dependencies": { "express": "^5.0.0" }}This is the recommended approach for new projects.
When You Might Still Use CommonJS
There are still valid reasons to use CommonJS in 2025:
1. Maintaining Legacy Codebases
If you have a large CommonJS codebase, migrating to ESM is time-consuming and risky. If it works, there’s no urgent need to migrate.
2. Node.js-Specific Tools
Some Node.js tools and packages only support CommonJS. Check before starting a new project if your critical dependencies have ESM support.
3. Dynamic Requires
If you need to require files dynamically at runtime based on conditions that can’t be known at build time, CommonJS makes this straightforward:
const drivers = { mysql: () => require('./drivers/mysql'), postgres: () => require('./drivers/postgres'),};
module.exports = drivers[process.env.DB_TYPE]();This is harder with ESM, though not impossible.
Migrating from CommonJS to ESM
If you’re ready to migrate, here’s a practical approach:
Step 1: Update package.json
{ "type": "module"}Step 2: Convert require to import
// Before (CommonJS)const express = require('express');const { Router } = require('express');
// After (ESM)import express from 'express';import { Router } from 'express';Step 3: Convert module.exports to export
// Before (CommonJS)module.exports = app;module.exports.handler = () => {};
// After (ESM)export default app;export const handler = () => {};Step 4: Handle File Paths
ESM requires explicit file extensions in import paths:
// This won't work in ESMimport utils from './utils'; // ❌
// This worksimport utils from './utils.js'; // ✓Step 5: Check Native Module Compatibility
Some Node.js built-in modules work differently. Notably, __dirname and __filename don’t exist in ESM:
// Before (CommonJS)const path = require('path');console.log(__dirname);
// After (ESM)import { fileURLToPath } from 'url';import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);const __dirname = dirname(__filename);The Bottom Line
In 2025, there’s no real debate for new projects. ESM is the modern JavaScript standard, and Node.js has mature support for it. All new Node.js projects should:
- Add
"type": "module"topackage.json - Use ESM syntax (
import/export) - Use the
.jsextension (or.mjsif you prefer)
CommonJS isn’t going away anytime soon, but it’s now clearly the legacy choice. If you’re starting fresh, ESM is the way to go.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments