Close Menu
ToolTechBlogToolTechBlog

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    Google’s Genie 4 world model adds audio, more realistic worlds, and real-time steering

    October 27, 2026

    OpenAI announces new discoveries in mathematics, computer science and physics

    October 27, 2026

    X is nudging users to promote their posts with a pointing hand, and people are confused

    October 27, 2026
    Facebook X (Twitter) Instagram
    ToolTechBlogToolTechBlog
    • Home
    • AI Tools
    • Web Hosting
    • Tech
    • Digital Marketing
    • Business Software
    • VPN & Cybersecurity
    ToolTechBlogToolTechBlog
    Home»Web Hosting»Format Differences and Migration Guide
    Web Hosting

    Format Differences and Migration Guide

    Tool Tech TeamBy Tool Tech TeamOctober 17, 2026No Comments14 Mins Read
    Facebook Twitter Pinterest Telegram LinkedIn Tumblr WhatsApp Email
    Format Differences and Migration Guide
    Share
    Facebook Twitter LinkedIn Pinterest Telegram Email

    ESM vs CommonJS in Node: When to Switch and How to Migrate

    <img src="https://tooltechblog.com/wp-content/uploads/2026/10/1584945800Group-5-14.png” alt=”SitePoint Team”>

    SitePoint TeamPublished inJavaScript·Programming·
    October 16, 2026

    The AI briefing for Developers

    Stay up to date with AI tools, model releases, and developer workflows that matter.

    Weekly. Free. One click to leave.

    SitePoint Premium
    Stay Relevant and Grow Your Career in Tech

    • Premium Results
    • Publish articles on SitePoint
    • Daily curated jobs
    • Learning Paths
    • Discounts to dev tools

    7 Day Free Trial. Cancel Anytime.

    Most Node.js projects still ship CommonJS. That is not a controversial claim; it reflects the inertia of millions of packages built before ES Modules were stabilized in Node 12.22, 14.17 and 15.3. But the ecosystem surrounding those projects — npm packages dropping CJS support, TypeScript defaulting to module settings that emit ESM, bundlers optimizing static import graphs — is accelerating toward ESM-only output. For intermediate Node.js developers maintaining server-side or full-stack production codebases, the question is no longer whether ESM will dominate but when migrating makes sense and how to do it without fracturing a dependency graph or stalling a CI pipeline.

    This guide covers the technical differences between CommonJS and ESM in Node.js, the resolution rules that determine how each file is parsed, a checklist for deciding when to migrate, and a staged conversion strategy with concrete error-to-fix troubleshooting references.

    Prerequisites: This guide assumes a currently supported Node.js release (Node 22 or Node 24 LTS recommended; Node 18 and 20 have reached end of life). Some features noted below require newer versions, which are called out inline. Examples use bash/zsh shell syntax unless otherwise noted.

    Table of Contents

    How Node.js Determines Module Format

    Node.js does not guess a file’s module format. It checks file extensions, the nearest package.json configuration, and, in narrow cases, a CLI flag.

    File Extensions: .mjs, .cjs, and .js

    Files ending in .mjs are always parsed as ES Modules, and files ending in .cjs are always parsed as CommonJS. Files ending in .js are ambiguous on their own; Node.js resolves them by consulting the nearest package.json up the directory tree.

    project/├── lib/│   ├── utils.mjs│   ├── config.cjs│   └── index.js└── package.json

    This means that .mjs and .cjs serve as escape hatches. A single CommonJS configuration file can coexist inside an otherwise ESM project simply by using the .cjs extension, and vice versa.

    The type Field in package.json

    The "type" field in package.json controls how .js files within its scope are parsed. Setting "type": "module" causes all .js files under that package.json to be treated as ESM. Setting "type": "commonjs", or omitting the field entirely, causes them to be treated as CommonJS.

    {"name":"my-esm-app","type":"module","version":"1.0.0"}

    Any .js file within this package’s directory tree will be parsed as an ES Module. import and export syntax is expected; require() is not available.

    {"name":"my-cjs-app","type":"commonjs","version":"1.0.0"}

    Here, .js files are parsed as CommonJS. require(), module.exports, __dirname, and __filename all work as expected.

    The scope boundary is the nearest package.json up the directory tree. In monorepos, this means each workspace with its own package.json controls its own module format independently; the root "type" field does not cascade into workspaces that define their own package.json.

    The --input-type Flag for Non-File Input

    The --input-type flag applies only to non-file input: --eval, --print, and STDIN. It does not affect files on disk.

    node --input-type=module --eval"import { readFile } from 'node:fs/promises';import { fileURLToPath } from 'node:url';const target = fileURLToPath(new URL('package.json', import.meta.url));console.log(await readFile(target, 'utf8'));"

    Note: The --eval snippet above resolves package.json relative to import.meta.url rather than the working directory. This avoids reading an unintended file if the script is invoked from a different CWD. On Windows PowerShell, use a single-line command or a script file instead of multi-line shell syntax.

    Without --input-type=module, the import statement in --eval would trigger a syntax error because Node.js defaults to CommonJS parsing for evaluated strings.

    Syntax and Behavioral Differences: CommonJS vs ES Modules

    Exports and Imports

    CommonJS uses require() for loading and module.exports or exports for exposing values. ESM uses import declarations and export statements. The Node.js modules documentation covers the syntax in detail, but the behavioral implications matter most during migration.

    const{ join }=require('node:path');functionresolvePath(dir, file){if(typeof dir !=='string'||typeof file !=='string'){thrownewTypeError(`resolvePath expects strings, got:${typeof dir},${typeof file}`);}returnjoin(dir, file);}module.exports={ resolvePath };
    import{ join }from'node:path';exportfunctionresolvePath(dir, file){if(typeof dir !=='string'||typeof file !=='string'){thrownewTypeError(`resolvePath expects strings, got:${typeof dir},${typeof file}`);}returnjoin(dir, file);}

    Named exports, default exports, and re-exports (export { foo } from './bar.js') all have direct ESM syntax. CommonJS achieves named exports through object properties on module.exports, which is less amenable to static analysis.

    Top-Level await, __filename, __dirname, and this

    ESM supports top-level await (Node 14.8+ unflagged), allowing modules to perform asynchronous operations at the top scope during evaluation. CommonJS does not support this; asynchronous work at the top level requires callbacks or wrapping in an immediately invoked async function.

    The __filename and __dirname globals are not available in ES Modules. The standard replacement uses import.meta.url combined with Node.js built-in utilities (import.meta.url is a file:// URL; fileURLToPath converts it to an OS path string):

    import{ fileURLToPath }from'node:url';import{ dirname }from'node:path';const __filename =import.meta.filename??fileURLToPath(import.meta.url);const __dirname =import.meta.dirname??dirname(__filename);

    import.meta.dirname and import.meta.filename are available natively in Node 21.2+ and Node 20.11+, and the Node docs mark them stable from 22.16 and 24.0. The snippet above uses ?? to fall back to the fileURLToPath pattern on older releases, so it works across all supported versions.

    At module scope, this is undefined in ESM, whereas in CommonJS it refers to module.exports.

    Loading Semantics: Synchronous vs Static Resolution

    require() is synchronous. It reads the file, evaluates it, and returns the cached module.exports object. Once loaded, subsequent require() calls return the cached reference. This synchronous nature enables conditional loading patterns like const db = process.env.DB === 'pg' ? require('pg') : require('mysql2').

    Node.js statically analyzes and resolves ESM import declarations at link time, before any module code executes; unlike require(), they cannot be deferred to runtime.

    Node.js resolves the module graph at link time, not at runtime. ESM named exports provide live bindings: if the exporting module updates a named export’s value, importing modules see the change. This does not apply to values capturede of require()

    The practical implication is that conditional loading in ESM requires dynamic import(), which returns a promise: const { default: pg } = await import('pg').

    The exports and imports Maps in package.json

    Conditional Exports for Dual-Format Packages

    The "exports" field in package.json lets package authors define entry points with conditions. This lets dual-format packages serve ESM to import consumers and CommonJS to require consumers.

    {"name":"my-library","version":"2.0.0","type":"module","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./utils":{"import":"./dist/utils.js","require":"./dist/utils.cjs"}}}

    The "types" condition should appear before "default" so that TypeScript resolves type declarations correctly. This ordering is required for TypeScript 4.7+ with moduleResolution: nodenext; earlier versions do not read exports-mapped types at all. If you need to support TypeScript versions before 4.7, add a top-level "types" field in package.json alongside the "exports" map as a fallback. Subpath exports like "./utils" control the public API surface, preventing consumers from importing internal files not explicitly listed.

    Self-Referencing with imports

    The "imports" field defines private import aliases scoped to the package itself. Entries must start with # to distinguish them from bare specifiers. For packages that need to support both ESM and CJS consumers (dual-format), use conditional fields:

    {"imports":{"#utils":{"import":"./src/utils.js","require":"./src/utils.cjs","default":"./src/utils.js"},"#db":{"import":"./src/db/client.js","require":"./src/db/client.cjs","default":"./src/db/client.js"}}}

    Inside the package, these aliases work as bare specifiers:

    import{ connect }from'#db';import{ formatDate }from'#utils';

    This avoids brittle relative paths like ../../../src/utils.js and centralizes path mappings in one place. If your package is ESM-only (not dual-format), simple string values without conditional fields are sufficient.

    Decision Table: Should You Migrate?

    CriterionStay on CommonJSMigrate to ESMNotes
    Project typeInternal app with no public APILibrary published to npmLibraries should ship ESM or dual-format to avoid forcing consumers into interop workarounds
    Node.js version floorNode < 16 requiredNode 16+ (LTS) or laterNode 12.22 and 14.17 stabilized the ESM implementation, but top-level await requires Node 14.8+, and practical tooling support solidified around Node 16. Node 12 and 14 are end-of-life.
    Dependency ESM readinessKey dependencies lack ESMMajor dependencies ship ESMCheck each dependency’s package.json directly or use publint
    TypeScript config"module": "commonjs", no plans to change"module": "nodenext" or "node16"Requires both "module" and "moduleResolution" set to "nodenext" (TypeScript 4.7+)
    Top-level await needNot neededRequired for startup logicOnly available in ESM (Node 14.8+ unflagged)
    Bundler involvementOutput is server-only, unbundledBundler already in pipeline (Vite, Rollup, esbuild)Bundlers handle ESM natively and tree-shake more effectively
    Team familiarityTeam has not shipped an ESM package or debugged ERR_MODULE_NOT_FOUND in productionTeam comfortable with import/export and full specifiersMigration without understanding the resolution model generates hard-to-debug errors
    Maintenance horizonEnd-of-life or frozen codebaseActively maintained for 2+ yearsESM-only dependencies will increasingly force the issue

    If you publish to npm, ship ESM now. Applications can migrate incrementally, module by module. Legacy projects pinned to Node versions below 16 should remain on CommonJS until they can upgrade their runtime floor.

    Step-by-Step Migration Guide

    Step 1: Audit Dependencies and Entry Points

    Before changing any configuration, catalog every require() call, entry point, and script in the project. Automated tools such as cjs-to-esm or @babel/plugin-transform-modules-commonjs (in reverse) can flag conversion candidates — verify each tool’s maintenance status and test on a branch before applying to production. Manually review test runners, build scripts, and tooling configuration files, many of which (Jest configs, Babel configs, Webpack configs) rely on CommonJS by default.

    Step 2: Set "type": "module" and Rename Holdouts

    This step will break any file that still uses require() or module.exports. Have your test suite ready before proceeding.

    Add "type": "module" to the root package.json. Then rename any files that must remain CommonJS to .cjs. Common holdouts include tool configuration files that do not yet support ESM.

    // package.json{"name": "my-app",+"type": "module","version": "3.1.0"}

    Only rename config files that actually use CJS syntax. Verify which config files exist first (e.g., ls .eslintrc*), since your project may use a different format (.json, flat config) or may not have these files at all.

    mv .eslintrc.js .eslintrc.cjsmv jest.config.js jest.config.cjs

    Note: Jest does not auto-discover .cjs config files in all versions. After renaming, either pass the config path explicitly (npx jest --config jest.config.cjs) or add "jest": {"config": "jest.config.cjs"} to your package.json to ensure Jest uses the correct configuration.

    Step 3: Convert require / module.exports to import / export

    Replace require() calls with import declarations. Replace module.exports with export statements. Convert dynamic require() to await import().

    const express =require('express');const{ readFile }=require('node:fs/promises');const router =require('./routes/api');const app =express();app.use('/api', router);module.exports= app;
    importexpressfrom'express';importrouterfrom'./routes/api/index.js';const app =express();app.use('/api', router);exportdefault app;

    Note the .js extension on the relative import. ESM requires full specifiers for relative paths; bare specifier resolution only applies to packages listed in node_modules. If the original require('./routes/api') resolved to ./routes/api/index.js, the ESM import must be './routes/api/index.js' — ESM does not perform automatic directory index resolution. The readFile import from the CommonJS version is not carried over because it is not used in this snippet; only import what you use.

    Step 4: Update package.jsonexports for Libraries

    If the project is a published library, add a conditional "exports" map as shown in the earlier section. This allows consumers using either require() or import to resolve the correct format.

    Step 5: Update Tooling Configuration

    What does your test runner expect? This step varies by tool, so check each one before making bulk changes.

    TypeScript: Set "module" and "moduleResolution" to "nodenext" in tsconfig.json:

    {"compilerOptions":{"module":"nodenext","moduleResolution":"nodenext","target":"es2022","outDir":"./dist","declaration":true}}

    Jest: For Jest with ESM, configure transform: {} in jest.config.cjs to disable default Babel transforms. Scope the experimental flag to the Jest invocation rather than setting it globally in NODE_OPTIONS (global settings propagate to all child Node processes, including postinstall scripts):

    {"scripts":{"test":"NODE_OPTIONS=--experimental-vm-modules npx jest --config jest.config.cjs"}}

    See Jest ESM docs for the full setup. Alternatively, consider switching to Vitest or Node’s built-in test runner (node --test), both of which support ESM natively.

    ESLint: The flat config format (eslint.config.js) is ESM-compatible by default (ESLint 8.21+). If using the legacy .eslintrc format, rename it to .eslintrc.cjs.

    Step 6: Validate and Ship

    On Node 22.7+ and 20.19+, syntax detection is on by default: a .js file with no "type" field that contains import or export is treated as ESM. That can hide a missing "type": "module", so set the field explicitly. To confirm that no file relies on detection, run the test suite with --no-experimental-detect-module. Check the flag exists on your version:

    node--help2>&1|grep-F'detect-module'&&echo"flag available"||echo"flag not available on this version"

    Run the full test suite, build pipeline, and any integration tests. A CI smoke test checklist should include: confirming all entry points resolve, verifying dynamic imports load correctly, and ensuring no ERR_REQUIRE_ESM or ERR_MODULE_NOT_FOUND errors appear in logs.

    Interop Patterns: Using CJS from ESM and Vice Versa

    Importing CommonJS Modules in ESM

    CommonJS modules can be imported from ESM files using default import syntax. Named imports work only when Node.js can statically detect the CJS module’s export keys at parse time — specifically, when the module assigns a static object literal to module.exports. Dynamic assignment (e.g., building the exports object conditionally at runtime) forces default-import-only.

    import_from'lodash';const{ groupBy, sortBy }= _;

    The destructuring happens at runtime rather than through static named imports because lodash’s CommonJS exports are assigned dynamically to module.exports. Note that this pattern imports the entire lodash bundle at runtime, which defeats tree-shaking. For ESM-native tree-shaking, use import { groupBy } from 'lodash-es' if your project can take the lodash-es dependency.

    Requiring ES Modules in CommonJS

    Historically, require() could not load ES Modules at all. Current Node releases can: require(esm) is enabled without a flag from Node 22.12 and 20.19, and Node 22.13 and 20.19 stopped printing the experimental warning (see the Node.js modules docs). The call returns the module namespace object, so a default export is under .default. It throws ERR_REQUIRE_ASYNC_MODULE if the ESM graph uses top-level await; use import() in that case.

    For older versions, the fallback is dynamic import() inside an async context:

    asyncfunctionloadConfig(timeoutMs =5000){const timer =newPromise((_, reject)=>setTimeout(()=>reject(newError('Config load timed out')), timeoutMs));const loader =import('./config.js').then((m)=> m.default);const config =awaitPromise.race([loader, timer]);if(config ===null|| config ===undefined){thrownewError('Config module exported nullish default');}return config;}

    This works on all Node.js versions with ESM support but requires callers to handle the returned promise. The timeout prevents the process from hanging indefinitely if the config module fails to load, and the explicit error handling avoids unhandled promise rejections (which crash the process in Node 15+).

    Error-to-Fix Troubleshooting Table

    Error MessageCauseFix
    ERR_REQUIRE_ESMCJS file require()-ing an ESM-only package on a Node release without require(esm)On Node 22.12+ and 20.19+ require() loads most ESM, and this error now mostly appears on older runtimes or when the package uses top-level await (ERR_REQUIRE_ASYNC_MODULE). Switch to dynamic import() or upgrade Node
    SyntaxError: Cannot use import statement outside a module.js file parsed as CJS but contains import syntaxAdd "type": "module" to package.json or rename the file to .mjs
    ReferenceError: __dirname is not definedESM does not provide __dirnameUse import.meta.dirname (Node 20.11+/21.2+) or dirname(fileURLToPath(import.meta.url))
    ERR_UNKNOWN_FILE_EXTENSIONMissing "type" field or custom loader misconfigurationSet "type" in the nearest package.json or use .mjs/.cjs extensions explicitly
    ERR_MODULE_NOT_FOUND (missing extension)ESM requires full specifiers including file extensionsAdd .js extension to all relative import paths
    ERR_PACKAGE_PATH_NOT_EXPORTEDImported subpath not listed in the package’s "exports" mapImport only entry points the package explicitly exports. If the subpath is essential, use patch-package to apply a local patch, or open an issue with the package maintainer.

    CommonJS vs ESM with TypeScript

    TypeScript mirrors Node.js module resolution through its "module" and "moduleResolution" compiler options. Setting "module": "nodenext" (or "node16") causes TypeScript to emit ESM when the enclosing package.json has "type": "module" and CommonJS otherwise. Setting both explicitly keeps the config clear (newer TypeScript versions infer moduleResolution from module).

    The .mts extension produces .mjs output, and .cts produces .cjs output, matching Node.js’s extension-based format detection. This allows individual files to opt into a different format than the package default, exactly as .mjs and .cjs do at runtime.

    For dual-format library publishing, the build pipeline typically produces two output directories or uses file extensions to differentiate formats. The package.json"exports" map then points each condition to the correct file, with "types" entries referencing .d.ts and .d.cts declarations respectively.

    Make the Switch Deliberately

    Migrate libraries first, since they impose format constraints on every downstream consumer. Applications can migrate incrementally when their tooling and dependencies are ready. Use the decision table above to justify timing rather than defaulting to either inertia or hype.

    CommonJS is not deprecated and remains stable, but the signals point one direction: packages like chalk, got, and execa have dropped CJS entirely, and TypeScript’s nodenext resolution treats ESM as the primary target. Waiting is fine. Waiting without a plan is not.

    For the canonical reference on Node.js module resolution, package configuration, and interoperability rules, consult the Node.js Packages documentation.

    Sharing our passion for building incredible internet things.

    Differences Format Guide Migration
    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email
    Tool Tech Team
    • Website

    Related Posts

    Browser Fingerprinting Explained: How Websites Identify You and How Developers Can Reduce It

    October 27, 2026

    Best AI Recruiting Agents in 2026: 6 Compared

    October 26, 2026

    7 Engineering Patterns for Measuring Traffic From AI Search and Chat Tools

    October 26, 2026

    9 Checks Before Shipping a Product Email: A Workflow for Developers and Designers

    October 25, 2026

    7 Checks Developers Should Make So AI Search Can Read Their Site

    October 25, 2026

    Best AI Recruiting Tools for Agencies and Staffing Firms in 2026

    October 24, 2026
    Leave A Reply Cancel Reply

    Top posts
    AI Tools

    Google’s Genie 4 world model adds audio, more realistic worlds, and real-time steering

    By Tool Tech Team
    Tech

    OpenAI announces new discoveries in mathematics, computer science and physics

    By Tool Tech Team
    Business Software

    X is nudging users to promote their posts with a pointing hand, and people are confused

    By Tool Tech Team
    Editors Picks

    Google’s Genie 4 world model adds audio, more realistic worlds, and real-time steering

    October 27, 2026

    OpenAI announces new discoveries in mathematics, computer science and physics

    October 27, 2026

    X is nudging users to promote their posts with a pointing hand, and people are confused

    October 27, 2026

    Browser Fingerprinting Explained: How Websites Identify You and How Developers Can Reduce It

    October 27, 2026
    About Us

    Welcome to ToolTechBlog, your trusted source for the latest insights, reviews, and practical guides on AI tools, business software, cybersecurity, web hosting, and consumer technology.
    Our mission is simple: to help individuals, entrepreneurs, freelancers, students, and businesses discover the right digital tools to improve productivity, streamline workflows, and make informed technology decisions.

    Our Picks

    Google’s Genie 4 world model adds audio, more realistic worlds, and real-time steering

    October 27, 2026

    OpenAI announces new discoveries in mathematics, computer science and physics

    October 27, 2026

    X is nudging users to promote their posts with a pointing hand, and people are confused

    October 27, 2026
    Top Reviews

    The AI Hype Index: Unsexy AI

    July 29, 2026

    What it is and How to Fix it

    July 29, 2026

    LG to Ban Residential Proxies from Smart TV Apps

    July 29, 2026

    © 2026 tooltechblog.com. All rights reserved. Designed by DD.

    • About Us
    • Contact Us
    • Terms and Conditions
    • Privacy Policy
    • Disclaimer

    Type above and press Enter to search. Press Esc to cancel.