Skip to main content
Node.js supports two module systems: the original CommonJS format and the ECMAScript modules (ESM) standard. Understanding how both work and interact is essential for modern Node.js development.

Module Systems Overview

Node.js implements two distinct module systems with different semantics:
1

CommonJS (CJS)

The original Node.js module system using require() and module.exports. Synchronous, dynamic loading with runtime resolution.
2

ES Modules (ESM)

The standardized JavaScript module system using import and export. Asynchronous, static structure with compile-time optimization.

CommonJS Modules

CommonJS is the default module system in Node.js, designed for synchronous server-side loading.

Module Wrapper

Every CommonJS module is wrapped in a function before execution:
// From lib/internal/modules/cjs/loader.js
const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});',
];
Your module code is wrapped like this:
(function (exports, require, module, __filename, __dirname) {
  // Your module code here
  const foo = 'bar';
  module.exports = foo;
});
The wrapper function provides the exports, require, module, __filename, and __dirname variables that appear to be global but are actually local to each module.

The require() Function

The require() function is the heart of CommonJS module loading:
// From lib/internal/modules/cjs/loader.js
Module.prototype.require = function(id) {
  validateString(id, 'id');
  if (id === '') {
    throw new ERR_INVALID_ARG_VALUE('id', id, 'must be a non-empty string');
  }
  return Module._load(id, this, /* isMain */ false);
};

Module._load() Internals

The internal loading process follows these steps:
1

Check Module Cache

First, check if the module is already loaded in Module._cache.
2

Check Native Modules

If it’s a built-in module like ‘fs’ or ‘http’, load it directly.
3

Create New Module

If not cached, create a new Module instance and add it to the cache.
4

Load Module Content

Read and compile the module file, wrapping it in the module wrapper.
5

Execute Module

Run the wrapped function, providing the module context.
6

Return Exports

Return the module.exports object to the caller.
// From lib/internal/modules/cjs/loader.js - simplified
Module._load = function(request, parent, isMain) {
  // 1. Resolve filename
  const filename = Module._resolveFilename(request, parent, isMain);
  
  // 2. Check cache
  const cachedModule = Module._cache[filename];
  if (cachedModule !== undefined) {
    return cachedModule.exports;
  }
  
  // 3. Check if native module
  const mod = loadBuiltinModule(filename, request);
  if (mod?.canBeRequiredByUsers) return mod.exports;
  
  // 4. Create new module
  const module = new Module(filename, parent);
  
  // 5. Add to cache before loading (for circular dependencies)
  Module._cache[filename] = module;
  
  // 6. Load the module
  module.load(filename);
  
  // 7. Return exports
  return module.exports;
};
Modules are cached after the first load. Subsequent require() calls for the same module return the cached exports object, not re-executing the module code.

Module Resolution Algorithm

Node.js uses a sophisticated algorithm to resolve module paths:
// From lib/internal/modules/cjs/loader.js
Module._resolveFilename = function(request, parent, isMain, options) {
  // Check if it's a built-in module
  if (BuiltinModule.canBeRequiredByUsers(request)) {
    return request;
  }
  
  // Resolve paths
  const paths = Module._resolveLookupPaths(request, parent);
  const filename = Module._findPath(request, paths, isMain, false);
  
  if (!filename) {
    throw new ERR_MODULE_NOT_FOUND(request, parent?.filename);
  }
  
  return filename;
};
  1. Built-in modules: Check if it matches a core module name
  2. Absolute paths: If starts with ’/’, use as-is
  3. Relative paths: If starts with ’./’ or ’../’, resolve relative to parent
  4. node_modules: Search up the directory tree for node_modules folders
  5. Extensions: Try .js, .json, .node if no extension specified
  6. Package main: Check package.json “main” field for directories
  7. Index files: Try index.js, index.json, index.node

Circular Dependencies

CommonJS handles circular dependencies by returning partially-filled exports:
// a.js
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done =', b.done);
exports.done = true;
console.log('a done');

// b.js
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done =', a.done);
exports.done = true;
console.log('b done');

// main.js
const a = require('./a.js');
const b = require('./b.js');

// Output:
// a starting
// b starting
// in b, a.done = false  ← a is not finished!
// b done
// in a, b.done = true
// a done
When module A requires module B, and B requires A, Node.js returns the incomplete exports object from A to break the cycle. The module is already in the cache, preventing infinite recursion.

ES Modules (ESM)

ES modules are the JavaScript standard for modules, introduced in ECMAScript 2015.

Enabling ESM

There are several ways to enable ESM in Node.js:
  1. Use .mjs file extension
  2. Set "type": "module" in package.json
  3. Use --input-type=module flag (for eval)

Import/Export Syntax

// Named exports
export const foo = 'bar';
export function myFunction() {}
export class MyClass {}

// Default export
export default function() {}

// Re-exporting
export { something } from './other-module.js';
export * from './another-module.js';
// Named imports
import { foo, myFunction } from './module.js';

// Default import
import myDefault from './module.js';

// Namespace import
import * as myModule from './module.js';

// Side-effect import
import './module.js';

// Dynamic import
const module = await import('./module.js');

ESM Loader Implementation

The ESM loader is more complex than CommonJS due to its asynchronous nature:
// From lib/internal/modules/esm/loader.js
class ModuleLoader {
  async import(specifier, parentURL) {
    // 1. Resolve
    const resolved = await this.resolve(specifier, parentURL);
    
    // 2. Load
    const loaded = await this.load(resolved.url);
    
    // 3. Instantiate
    const module = await this.instantiate(loaded);
    
    // 4. Evaluate
    await module.evaluate();
    
    return module.namespace;
  }
}
ESM loading is asynchronous because it may need to fetch modules from the network (in browsers) or perform other async operations. Node.js maintains this behavior for consistency with the standard.

Static vs Dynamic

ESM imports are statically analyzable:
// ✓ GOOD: Static import - analyzable at parse time
import { readFile } from 'fs';

// ✗ BAD: Not allowed - imports must be at top level
if (condition) {
  import { readFile } from 'fs'; // SyntaxError
}

// ✓ GOOD: Use dynamic import for conditional loading
if (condition) {
  const { readFile } = await import('fs');
}
Static imports enable tree-shaking and other optimizations. Use dynamic import() only when you need runtime conditional loading.

Module Graph

ESM builds a module graph before execution:
// From lib/internal/modules/esm/module_job.js
class ModuleJob {
  async link() {
    // Link all dependencies recursively
    const promises = this.module.linking.map(
      (dep) => dep.moduleJob.link()
    );
    await Promise.all(promises);
  }
  
  async evaluate() {
    // Evaluate in post-order (dependencies first)
    await this.module.evaluate();
  }
}

CommonJS vs ES Modules

FeatureCommonJSES Modules
Syntaxrequire() / module.exportsimport / export
LoadingSynchronousAsynchronous
StructureDynamicStatic
CachingCached, can be clearedCached, immutable
ContextModule wrapper functionModule scope
thismodule.exportsundefined
File extension.js, .cjs.mjs, .js (with type:module)
Top-level awaitNoYes
Conditional loadingYes (runtime)Dynamic import only

Interoperability

ES modules can import CommonJS modules, but only the default export:
// cjs-module.js (CommonJS)
module.exports = {
  foo: 'bar',
  baz: 42
};

// esm-module.mjs (ES Module)
import cjsModule from './cjs-module.js';
console.log(cjsModule.foo); // 'bar'

// Named imports from CJS require special handling
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjs = require('./cjs-module.js');
CommonJS cannot use synchronous require() for ESM. Use dynamic import:
// This does NOT work
const esm = require('./esm-module.mjs'); // Error

// Use dynamic import instead
async function loadESM() {
  const esm = await import('./esm-module.mjs');
  console.log(esm.default);
}
You cannot require() an ES module. The require() function is synchronous, but ESM loading is asynchronous. Always use dynamic import() to load ESM from CommonJS.

Package.json Fields

The package.json file controls module resolution:

“type” Field

{
  "type": "module"
}
  • "type": "module": Treat .js files as ES modules
  • "type": "commonjs" (or omitted): Treat .js files as CommonJS

”exports” Field

{
  "exports": {
    ".": {
      "import": "./index.mjs",
      "require": "./index.cjs"
    },
    "./feature": {
      "import": "./feature/index.mjs",
      "require": "./feature/index.cjs"
    }
  }
}
The “exports” field provides fine-grained control over what can be imported from your package and enables different entry points for ESM vs CommonJS.

”main” Field

{
  "main": "./index.js"
}
The “main” field specifies the entry point for CommonJS requires. It’s used when “exports” is not defined.

Module Caching

CommonJS Cache

// From lib/internal/modules/cjs/loader.js
Module._cache = ObjectCreate(null);

// You can access and manipulate it
console.log(require.cache);

// Clear a specific module from cache
delete require.cache[require.resolve('./my-module.js')];

ESM Cache

ESM modules are cached in an internal module map that cannot be accessed or cleared:
// From lib/internal/modules/esm/module_map.js
class ModuleMap {
  constructor() {
    this.map = new SafeMap();
  }
  
  get(url) {
    return this.map.get(url);
  }
  
  set(url, job) {
    this.map.set(url, job);
  }
}
ESM module cache is not exposed to user code. Once an ES module is loaded, it cannot be reloaded or cleared during the lifetime of the process.

Module Resolution Examples

Node Modules Resolution

require('lodash')

// Searches in:
// /home/user/project/node_modules/lodash
// /home/user/node_modules/lodash
// /home/node_modules/lodash
// /node_modules/lodash

Package Subpath

import debounce from 'lodash/debounce.js';

// With "exports" in package.json:
{
  "exports": {
    "./debounce.js": "./debounce.js"
  }
}
The “exports” field in package.json allows package authors to define which files can be imported, providing encapsulation and preventing users from accessing internal implementation details.

Import Assertions and Attributes

Node.js supports import attributes (formerly assertions) for JSON and other formats:
// JSON modules
import config from './config.json' with { type: 'json' };

// From lib/internal/modules/esm/assert.js
const { kImplicitTypeAttribute } = require('internal/modules/esm/assert');