Q22 of 40 · JavaScript

What is the difference between ES Modules and CommonJS, and why does it matter for test tooling?

JavaScriptMidjavascriptesmcommonjsmodulesnodejestplaywright

Short answer

Short answer: CommonJS (`require`/`module.exports`) is synchronous and dynamic. ES Modules (`import`/`export`) are static, asynchronous, and tree-shakeable. Jest historically defaults to CJS via Babel; Playwright and Vitest are ESM-native. Mixing systems causes interop friction that breaks test setups.

Detail

JavaScript has two module systems with different semantics.

CommonJS (CJS): Used by Node.js since its inception. require() is synchronous and evaluated at runtime — you can require inside an if block. module.exports carries the export. CJS modules execute once and cache; subsequent require() calls return the cached result.

ES Modules (ESM): The TC39 standard. import/export are static — parsed and resolved before any code runs. This enables static analysis and tree-shaking (unused exports eliminated at build time). Top-level await is supported. Use .mjs extension or "type": "module" in package.json.

Interop pain points:

  • Jest uses Babel to transform ESM to CJS by default; native ESM requires explicit Jest config.
  • Libraries shipping only ESM cannot be require()d without a workaround.
  • __dirname and __filename are absent in ESM (use import.meta.url + fileURLToPath instead).
  • Dynamic import() works in both systems and is the bridge for conditional/lazy imports.

// EXAMPLE

// CommonJS
const path = require("path");
module.exports = { helper };
function helper() { return "cjs"; }

// ES Module
import path from "path";
export function helper() { return "esm"; }
export default class Config {}

// __dirname in ESM
import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname  = dirname(__filename);

// Dynamic import — works in both contexts
const { default: Plugin } = await import(`./plugins/${name}.js`);

// WHAT INTERVIEWERS LOOK FOR

Static vs dynamic nature, tree-shaking implications, and real-world interop pain (Jest CJS vs ESM). Knowing the `__dirname` workaround or `import.meta.url` shows Node.js test setup experience.

// COMMON PITFALL

Adding `"type": "module"` to package.json without updating Jest config — Jest's default Babel transform breaks on native ESM and requires explicit `extensionsToTreatAsEsm` configuration.