AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 02 — Node.js Modules — Splitting Code into Files

02 — Node.js Modules — Splitting Code into Files

August 13, 20265 min read
Download as Markdown

"Just import what you need" was my modules mental model, and it skipped the split that explains most Node errors. The idea that everything else hangs off: a module is just a file. Node.js gives every file its own scope, and provides a module system so files can share their internals with each other. There are two such systems, and the friction in the Node.js ecosystem today is almost entirely the seam between them [1].

The framing that finally landed for me is the contrast — not the syntax in isolation.

CommonJS: the legacy default

CommonJS is the module system Node.js shipped with. It uses require() to import and module.exports (or exports) to export, and it is synchronous and dynamic — require is a real function that runs when the line executes, so the module is loaded at runtime [1][2].

// math.js — CommonJS
function add(a, b) {
return a + b;
}

module.exports = { add };
// app.js — CommonJS
const { add } = require('./math');
console.log(add(2, 3)); // 5

The whole file's exports live on the module.exports object, and require returns whatever that object is. The path matters — ./math is a local file, express (no dot) resolves through node_modules. CommonJS is still the default in any file that isn't explicitly flagged otherwise, and an enormous amount of existing Node.js code is written this way.

ESM: the modern standard

ESM (ECMAScript Modules) is the official JavaScript module standard, the same one browsers use. It uses import and export keywords, and it is static — imports are analyzed at parse time, before any code runs. That enables tree-shaking (dropping unused exports), better tooling, and it always runs in strict mode [3][4].

// math.mjs — ESM
export function add(a, b) {
return a + b;
}
// app.mjs — ESM
import { add } from './math.mjs';
console.log(add(2, 3)); // 5

The two systems look similar but behave differently in ways that bite. ESM imports are live bindings — the imported name always reflects the current value in the exporting module. ESM imports are hoisted — they happen before the rest of the file executes, regardless of where the line sits. And ESM is asynchronous by design, which is why require and import cannot be freely mixed.

How Node.js decides which system to use

The friction in practice is "which system is this file?" Node.js uses a set of rules:

  • A .mjs file is always ESM. A .cjs file is always CommonJS.
  • A .js file's type depends on the nearest package.json — if it has "type": "module", the file is ESM; otherwise it is CommonJS by default [3].
  • import in a CommonJS file throws; require of an ESM file throws.
{
"type": "module"
}

That one line in package.json flips every .js file in the package to ESM. New projects I start today default to ESM; the migration story for older CommonJS projects is real work because the two systems interop only through dynamic import() and createRequire.

Creating and importing: how it works

The model I keep is simple — every file is a module, and every module is wrapped in a function by Node before it runs. That wrapper is why the top level of a Node file is not the global scope. It is also why __dirname, __filename, require, module, and exports are available without me importing them — Node injects them as arguments to the wrapper.

// roughly what Node does with every CommonJS module
(function (exports, require, module, __filename, __dirname) {
// my module code lives here
});

That wrapper is the reason a top-level var in a Node module does not leak into other files. In a browser, var x at the top level becomes a property of window. In Node, it becomes a local variable inside the wrapper function [5].

The global keyword

The counterpart to the browser's window is Node's global object. Anything attached to global is visible in every module without an import — global.myThing = 1 is then available as myThing everywhere [5]. This is a power tool I avoid reaching for. Globals make code harder to test, harder to reason about, and easy to break by accident. The legitimate globals Node provides (process, console, Buffer, the timers) are the ones the runtime itself depends on; user code almost always does better with explicit imports.

The one detail worth knowing: in ESM, some of the CommonJS-only globals (__dirname, __filename, require) are not available, because the ESM wrapper is different. The ESM equivalents come from import.meta.url.

// ESM equivalent of __dirname
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

That snippet is the price of admission to ESM for any tool that needs absolute file paths.

How I use this

The decision check I use is "which system does this project live in?" — and I check package.json for "type": "module". For new code I default to ESM: it is the standard, it works in browsers and Node with the same syntax, and static analysis makes tooling better. For existing CommonJS projects I stay consistent rather than mixing freely, and I reach for dynamic import() only at the boundary. The model — a module is a wrapped file, two systems for sharing — is what keeps the friction manageable.

References

[1] OpenJS Foundation, "Modules: CommonJS modules," Node.js API Docs. [Online]. Available: https://nodejs.org/api/modules.html#modules-commonjs-modules

[2] O.坂本, "CommonJS vs. ES Modules in Node.js," LogRocket Blog, 2023. [Online]. Available: https://blog.logrocket.com/commonjs-vs-es-modules-node-js/

[3] OpenJS Foundation, "ECMAScript Modules," Node.js API Docs. [Online]. Available: https://nodejs.org/api/esm.html

[4] T. Eckert, "ES Modules in Node Today," LogRocket Blog. [Online]. Available: https://blog.logrocket.com/es-modules-in-node-today/

[5] OpenJS Foundation, "Globals," Node.js API Docs. [Online]. Available: https://nodejs.org/api/globals.html

Knowledge check · Question 1 of 4

What is the fundamental difference between CommonJS `require` and ESM `import`?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!