Skip to main content
Lead Generation Websites, Google Maps Ranking, WhatsApp Funnels, Ecommerce, SEO, Web DesignSpeed Optimization · Conversion Optimization · Monthly Lead Systems · AI AutomationLead Generation Websites, Google Maps Ranking, WhatsApp Funnels, Ecommerce, SEO, Web Design

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

Published: January 21, 2026
Written by Sumeet Shroff
JavaScript require() vs import: CommonJS vs ES Modules Explained (with Practical Examples)
Table of Contents
  1. Quick overview: CommonJS vs ES Modules
  2. What is the difference between require() and import?
  3. Example: CommonJS (require/module.exports)
  4. Example: ES Modules (import/export)
  5. Comparison table
  6. Node.js support and package.json configuration
  7. Default vs Named exports: module.exports vs export default
  8. Dynamic import() vs require()
  9. Top-level await basics
  10. Interop patterns (importing CJS in ESM and vice versa)
  11. How to migrate from CommonJS to ES Modules
  12. Bundlers and transpilation
  13. Performance differences
  14. When to use which in modern projects (Next.js, Vite, Node)
  15. Real-World Scenarios
  16. Scenario 1: Migrating a backend microservice from CommonJS to ESM
  17. Scenario 2: Lazy-loading a heavy UI component in Vite
  18. Scenario 3: Integrating a legacy NPM package into a modern ESM app
  19. Checklist
  20. Checklist
  21. Common errors and solutions (quick reference)
  22. Latest News & Trends
  23. Additional resources
  24. FAQ
  25. Key Takeaways
  26. Conclusion
  27. Further reading and authority sources
  28. About Prateeksha Web Design
In this guide you’ll learn
  • The core differences between require() (CommonJS) and import (ES Modules).
  • How Node.js, bundlers, and browsers treat modules and when to migrate.
  • Practical code examples: exports/imports, dynamic import(), top-level await, and interop patterns.

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) and module.exports.
  • ES Modules (ESM): standardized, uses import/export (static), supports tree-shaking and static analysis.
Tip Use ESM for new projects when possible — browsers and modern bundlers optimize better with static imports.

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.

FeatureCommonJS (require)ES Modules (import)
Syntaxrequire() / module.exportsimport / export
LoadingSynchronous at runtimeStatic imports (early), dynamic import() async
Tree-shakingNot supported nativelySupported by bundlers (rollup, webpack, Vite)
Interop complexitySimple for CJS ecosystemRequires interop for CJS <-> ESM
Node supportUniversalStable 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 .js as ESM.
  • Or use .mjs for ESM and .cjs for 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).
  • Error: require is not defined in ESM
    • Fix: Use dynamic import() or createRequire from 'module'.
Warning Switching the package `type` to `module` changes how all `.js` files are parsed. Test carefully and update tooling configs.

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) { return Hi ${name} }

Interop nuance:

  • Importing a CommonJS default export in ESM often gives you the module.exports object as the default import.
  • Many bundlers or Node use a synthetic default: import pkg from 'cjs-package' where pkg.default may 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.

Fact ES Modules enable static analysis which allows bundlers to perform tree-shaking and produce smaller bundles compared to CommonJS.

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

  1. Add tests and CI. Migration touches runtime semantics.
  2. Add "type":"module" OR rename files to .mjs gradually.
  3. Convert module.exports = to export default and exports.foo = to export const foo.
  4. Replace require() with import and handle dynamic cases with import().
  5. 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 / __filename are 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-env and 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 import and native ESM modules.
  • Node: Use ESM for new apps; if you depend on older CJS packages, keep interop layers or gradually migrate.
Tip For apps built with Vite or Rollup, keep `modules: false` in Babel to allow bundlers to apply tree-shaking.

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 .mjs vs "type":"module" approach.
  • Update imports to include file extensions where required.
  • Replace __dirname/__filename usage 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.
  • "require is not defined"
    • Use createRequire or dynamic import() in ESM modules.
  • Missing extensions error in ESM
    • Add explicit .js or .mjs file extensions in import paths.

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

Key takeaways
  • ES Modules (import/export) are the modern standard and enable static analysis and tree-shaking.
  • CommonJS (require/module.exports) remains widely used for legacy Node.js packages and simple scripts.
  • Node supports both: use `"type":"module"` or `.mjs` for ESM; use `.cjs` for explicit CJS files.
  • Use dynamic `import()` for lazy-loading and top-level await for async initialization in ESM.
  • Interop patterns and tooling (createRequire, dynamic import, bundler config) help bridge CJS and ESM during migration.

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

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.

Sumeet Shroff
Sumeet Shroff
Sumeet Shroff is a renowned expert in web design and development, sharing insights on modern web technologies, design trends, and digital marketing.

Comments

Leave a Comment

Loading comments...