JavaScript require() vs import: CommonJS vs ES Modules Explained (with Practical Examples)

JavaScript require() vs import: CommonJS vs ES Modules Explained (with Practical Examples)
Modules let you split code, reuse logic, and enable tooling optimizations. But JavaScript today has two dominant module systems: CommonJS (require/module.exports) and ES Modules (import/export). This guide explains differences, Node.js support, bundler behavior, common errors and fixes, and migration paths — with real examples you can use today.
Quick overview: CommonJS vs ES Modules
- CommonJS (CJS): historically used by Node.js. Uses
require()(synchronous) andmodule.exports. - ES Modules (ESM): standardized, uses
import/export(static), supports tree-shaking and static analysis.
What is the difference between require() and import?
- require() is a runtime, synchronous function that loads a module and returns its exports (CommonJS).
- import is a syntax-level construct (ESM) that is statically analyzable. Static imports run before module body execution and enable tree-shaking; dynamic
import()is asynchronous and returns a Promise.
Example: CommonJS (require/module.exports)
// lib/math.js (CommonJS) function add(a, b) { return a + b } module.exports = { add }
// index.js const { add } = require('./lib/math') console.log(add(2, 3)) // 5
Example: ES Modules (import/export)
// lib/math.mjs or with "type": "module" in package.json -> lib/math.js export function add(a, b) { return a + b }
// index.mjs import { add } from './lib/math.js' console.log(add(2, 3)) // 5
Comparison table
Below is a concise comparison to help choose between them.
| Feature | CommonJS (require) | ES Modules (import) |
|---|---|---|
| Syntax | require() / module.exports | import / export |
| Loading | Synchronous at runtime | Static imports (early), dynamic import() async |
| Tree-shaking | Not supported natively | Supported by bundlers (rollup, webpack, Vite) |
| Interop complexity | Simple for CJS ecosystem | Requires interop for CJS <-> ESM |
| Node support | Universal | Stable in Node >= 12+ (recommended in modern Node) |
Node.js support and package.json configuration
Node supports both systems now, but you must tell Node how to interpret .js files in your package.json:
- Use "type": "module" to treat
.jsas ESM. - Or use
.mjsfor ESM and.cjsfor CommonJS explicitly.
package.json examples:
// ESM mode { "type": "module" }
// CJS mode (default), or set files to .cjs/.mjs { "type": "commonjs" }
Common errors and fixes:
- Error:
SyntaxError: Cannot use import statement outside a module- Fix: Add
"type": "module"to package.json, rename file to.mjs, or use a transpiler (Babel).
- Fix: Add
- Error:
require is not definedin ESM- Fix: Use dynamic
import()orcreateRequirefrom'module'.
- Fix: Use dynamic
Default vs Named exports: module.exports vs export default
CommonJS:
// single export module.exports = function greet(name) { return `Hi ${name}` }
// multiple exports module.exports = { greet, // named property }
ESM:
// default export export default function greet(name) { return `Hi ${name}` }
// named export export function greet(name) { returnHi ${name}}
Interop nuance:
- Importing a CommonJS default export in ESM often gives you the
module.exportsobject as the default import. - Many bundlers or Node use a synthetic default:
import pkg from 'cjs-package'wherepkg.defaultmay exist depending on how it was exported.
Example: importing CJS from ESM
// commonjs-lib.js module.exports = { foo: 1 }
// esm.mjs import pkg from './commonjs-lib.js' console.log(pkg.foo) // 1
Example: importing ESM from CommonJS
// esm-lib.mjs export const foo = 1
// require-bridge.cjs (async () => { const esm = await import('./esm-lib.mjs') console.log(esm.foo) })()
Dynamic import() vs require()
require()is synchronous and returns the module immediately — good for simple server-side code but not for code-splitting.import()is asynchronous and returns a Promise — ideal for lazy-loading modules in the browser and Node.
Example: dynamic import for lazy load
// client-side or Node
async function loadFeature() {
const { heavy } = await import('./heavy-feature.js')
heavy.doWork()
}
Bundlers like Vite and webpack support
import()for code-splitting automatically.
Top-level await basics
ESM supports top-level await in modules. This enables simple asynchronous initialization without wrapping in async functions.
// config.mjs const resp = await fetch('/config.json') export const config = await resp.json()
// app.mjs import { config } from './config.mjs' console.log(config)
Node requires the module to be ESM (or .mjs) for top-level await. Bundlers may handle it differently; check your bundler docs.
Interop patterns (importing CJS in ESM and vice versa)
Import CJS into ESM (common):
// ESM
import pkg from 'some-cjs-package'
// pkg is the module.exports object
Import ESM into CJS (less direct):
// CJS
(async () => {
const esm = await import('./esm-module.mjs')
console.log(esm.default)
})()
Use Node's createRequire to call require() from an ESM module if needed:
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const legacy = require('./legacy.cjs')
Common pitfall: default vs named mapping. If a CJS module sets module.exports = function () {}, then import fn from 'cjs' is the default import. But if module.exports = { a: 1 }, that object becomes the default import.
How to migrate from CommonJS to ES Modules
- Add tests and CI. Migration touches runtime semantics.
- Add
"type":"module"OR rename files to.mjsgradually. - Convert
module.exports =toexport defaultandexports.foo =toexport const foo. - Replace
require()withimportand handle dynamic cases withimport(). - Update build tools (Babel preset-env, webpack, or Vite) to support ESM.
Example small migration:
CommonJS:
// math.js
exports.add = (a, b) => a + b
ESM:
// math.js
export const add = (a, b) => a + b
Common errors during migration:
- Missing file extensions in imports: Node ESM requires full file extensions by default (
./math.js, not./math). Fix by adding extensions or enabling resolver in bundler. __dirname/__filenameare undefined in ESM. Use:
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
Bundlers and transpilation
- Webpack: supports both CJS and ESM; configuration may affect tree-shaking and module resolution.
- Rollup and Vite: optimized for ESM and tree-shaking by default.
- Babel: can transpile ESM to CJS (or keep ESM) using
@babel/preset-envand plugins.
If you need to transpile modules with Babel for older Node versions or browsers, configure modules: false to preserve ESM for bundlers, or modules: 'commonjs' to convert to CommonJS.
Performance differences
- ESM's static format enables better optimizations and tree-shaking. Cold-start may be faster for ESM in modern engines.
- CommonJS synchronous require can block execution but is simpler in small scripts.
In practice, choose ESM for long-term maintenance and bundler friendliness; choose CJS only for legacy compatibility or when using older tooling.
When to use which in modern projects (Next.js, Vite, Node)
- Next.js: prefers ESM in recent versions; server code can be ESM but many Next.js internals still interop with CJS. Follow framework docs.
- Vite: ESM-first — use
importand native ESM modules. - Node: Use ESM for new apps; if you depend on older CJS packages, keep interop layers or gradually migrate.
Real-World Scenarios
Scenario 1: Migrating a backend microservice from CommonJS to ESM
A team updated package.json to "type":"module" and converted a few utility files to export syntax. They encountered require is not defined in a legacy script and resolved it by isolating that script as .cjs until full migration was done.
Scenario 2: Lazy-loading a heavy UI component in Vite
An engineer used const module = await import('./heavy-component.js') to code-split. Vite generated a separate chunk and load times improved on first route transition.
Scenario 3: Integrating a legacy NPM package into a modern ESM app
A developer imported a CommonJS package into an ESM project; they had to access pkg.default for compatibility until they replaced the package or applied a bridge wrapper.
Checklist
Checklist
- Confirm project Node.js version supports the desired ESM features.
- Add tests and CI before changing package
type. - Decide on
.mjsvs"type":"module"approach. - Update imports to include file extensions where required.
- Replace
__dirname/__filenameusage with import.meta.url helpers. - Update bundler (Vite/webpack/Rollup) and Babel settings for module handling.
- Verify third-party dependencies for CJS/ESM compatibility.
Common errors and solutions (quick reference)
- "Cannot use import statement outside a module"
- Add
"type":"module"or rename to.mjs; or use Babel/webpack to transpile.
- Add
- "require is not defined"
- Use
createRequireor dynamicimport()in ESM modules.
- Use
- Missing extensions error in ESM
- Add explicit
.jsor.mjsfile extensions in import paths.
- Add explicit
Latest News & Trends
- Node continues improving ESM support and interop strategies; many packages publish both ESM and CJS builds.
- Tooling (Vite, esbuild) doubles down on ESM-first developer experience and much faster dev server reloads.
- Frameworks like Next.js and Remix are increasing support for ESM-based plugins and server runtimes.
Additional resources
FAQ
See the bottom of the article for an FAQ block with five frequently asked questions.
Key Takeaways
Conclusion
Choosing between require() and import depends on your project constraints: legacy dependencies, runtime targets, and tooling. For new projects, prefer ES Modules to benefit from modern tooling, tree-shaking, and clearer semantics. When migrating, proceed incrementally, use the interop patterns shown above, and validate behavior with tests and CI.
Further reading and authority sources
- MDN Web Docs — deep reference for import/export syntax and behaviors.
- Node.js ESM docs — official Node guidance on modules and interop.
- W3C Web Accessibility Initiative — accessibility best practices to include when refactoring UI and build pipelines.
About Prateeksha Web Design
Prateeksha Web Design builds modern Next.js applications and frontend architectures, focusing on clean module practices, ESM migration, performance optimizations, and accessible component libraries. We provide consulting, implementation, and training for scalable JavaScript codebases and long-term maintenance and developer workflows support.
Chat with us now Contact us today.