1.3 · Modules: CommonJS (require) vs ES Modules

Level 1 · JavaScript & Node basics

1.3Modules: CommonJS (require) vs ES Modules

Goal: split your code into modules and master both systems (CommonJS require/module.exports and ESM import/export), understanding the common principle.
Estimated time: 11 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

The essentials in 30 seconds

  • One module = one file, one responsibility: export what you share, import what you need — the basis of organisation.
  • CommonJS (historical, omnipresent): module.exports to export, require() to import — synchronous, simple, everywhere in npm.
  • ES Modules (standard, future): export/import, enabled via .mjs or "type": "module" — recommended for a new project, but you must read both.

Running mini-case — Salim splits his application in Algiers

Salim's application fits in a single 500-line file: calculation functions mixed with file reads, impossible to test. He splits it into three modules — calculations, data reading, display — each exporting its functions: first with CommonJS (module.exports/require) to understand, then he enables ES Modules ("type": "module") for his new project. Each module has a clear responsibility and is tested separately.

Continuation of case 1.1-1.2 — Salim structures his code after the language and asynchrony.

1. CommonJS: module.exports and require()

No serious program fits in a single file: you split into modules (files with one responsibility, which import each other). Node's historical system is CommonJS: you export with module.exports (module.exports = { fn1, fn2 }) and import with require() (const thing = require('./monModule')). Synchronous, simple, present in millions of npm packages. A local module starts with ./ or ../; a native or npm module is imported by name (require('fs')).

Salim creates calculs.js with module.exports = { additionner, soustraire } and imports it via require('./calculs'): his 500-line file starts breathing.

2. ES Modules: export and import

ES Modules are the standard syntax of the language (the same as in the modern browser): you export with export (export function fn() {}) and import with import (import { fn } from './autre.js'). You enable them by naming your files .mjs or — more common — by adding "type": "module" in package.json. ESM is recommended for a new project (standard, future, consistent with front-end).

Salim adds "type": "module" to his new project and switches to import { additionner } from './calculs.js' — noting the mandatory .js extension in the path.

3. Understand both, split well

CommonJS remains omnipresent (existing code, tutorials, packages): you will read a lot of require/module.exports even while writing import/export. Mixing both requires care (importing CommonJS from ESM works, the reverse is trickier). Beyond syntax, the key is the principle: split by responsibility, export a clear interface, name files explicitly, avoid circular dependencies.

Salim names his modules calculs.js, donnees.js, affichage.js and only exports what's needed: a new developer knows where to look without asking him.

Common misinterpretation: “require and import are interchangeable in the same file” or “just renaming to .mjs with no other change is enough”. No. Each file follows ONE system depending on the configuration (".type": "module" or not): mixing syntaxes without explaining it breaks execution. And switching to ESM requires adjustments (explicit extensions in paths, no __dirname in pure ESM) — it's not a simple rename.
Two module systems Same principle, different syntax CommonJS module.exports /require() — historicalES Modules export / import —language standardCommon principle Split,export, import
Same principle, different syntax
Text version of the diagram (SEO / screen readers / translation): 1) Split: Salim moves calculations, reads and display out of his single file into three modules with clear responsibility. 2) Export/import: with CommonJS via module.exports/require('./calculs'), with ESM via export/import with explicit extension. 3) Choose: ESM ("type": "module") for the new project, routinely reading existing CommonJS — without mixing both syntaxes in the same file. Lesson: the principle (split, export, import) matters more than the syntax.

🔎 Concrete example — 🔎 Concrete example — Export and import

First CommonJS, then ES Modules: two alternatives, one system per file.

// --- Alternative A: CommonJS (classic .js file) ---
// in calculs.js:
// module.exports = { additionner, soustraire };
// in app.js:
const calc = require("./calculs");
console.log(calc.additionner(2, 3));

Reading: the local module is imported via its ./ path, the package or native one by name.

Lesson: in ESM, the equivalent is export function additionner + import { additionner } from './calculs.js' — with the mandatory extension.

Section vocabulary

Module
Separate file with one responsibility, which exports what it makes available and imports what it needs; the basis of code organisation.

Simple example: Salim's calculs.js, donnees.js and affichage.js.

What it is not: a single 500-line file that does everything.

Reflex: one file = one clear responsibility.

CommonJS (CJS)
Node's historical module system: export with module.exports, import with require(). Synchronous, simple, omnipresent in the npm ecosystem.

Simple example: The require('./calculs') of Salim's first split.

What it is not: the standard language import/export syntax.

Reflex: read it fluently, even while writing ESM.

ES Modules (ESM)
The language-standard module system: export with export, import with import. Enabled via .mjs or "type":"module" in package.json.

Simple example: The import { additionner } from './calculs.js' of Salim's new project.

What it is not: a require() in a file configured as ESM.

Reflex: new project = ESM, explicit extension.

require vs import
Both import syntaxes: require() (CommonJS) and import (ESM). Both coexist in real code, hence the need to understand both.

Simple example: Salim reads require in old packages and writes import in his project.

What it is not: mixing them in the same file without suitable configuration.

Reflex: one system per file, chosen knowingly.

Module path
Local: starts with ./ or ../ (project files). Bare name: native module (fs) or npm package from node_modules.

Simple example: Salim's local require('./calculs') vs native require('fs').

What it is not: require('calculs') without ./ for a project file (won't be found).

Reflex: ./ for the project, bare name for system and npm.

In practice — Hands-on — Organise your code into modules

  1. Create a module exporting functions with CommonJS (module.exports = { fn1, fn2 }) and import it with require('./monModule').
  2. Recreate the same module in ESM (export function fn1() {}) and import it with import (explicit .js extension).
  3. Enable ESM in a project by adding "type": "module" to package.json, and observe that .js files become ES Modules.
  4. Import a native module (fs) and a local module (via ./path) — spot the path difference.
You split your code into modules and master both systems (CommonJS require/module.exports and ESM import/export), understanding the common organisational principle.

Ready-to-say script — propose splitting into modules (30 s)

“This file is 500 lines and three jobs: I suggest we split it into three modules — calculations, data, display — each with its exports. We'll be able to test each part separately and reuse them elsewhere.”

To say in code review; show the three-module plan before splitting.

Copy-paste template — CommonJS module + usage

// calculs.js: we export what we share
function additionner(a, b) {
  return a + b;
}
function soustraire(a, b) {
  return a - b;
}
module.exports = { additionner, soustraire };
// app.js: we import what we need
const calc = require("./calculs");
console.log(calc.additionner(2, 3));
console.log(calc.soustraire(5, 2));

Single file to split

Spot responsibilities (calculations, data, display), extract one module at a time checking with node at each step — like Salim with his 500 lines.

New project in Algiers

Salim starts with ESM ("type": "module"): same import/export syntax as the team's front-end, a single system to know.

Old require packages

Facing require-style documentation: know how to read and use it as is, without converting the whole project.

Team naming

A filename that says what it does (donnees.js, validation.js): the structure becomes free documentation for the next developer.

✓ Best practices

  • One module = one responsibility.
  • Only export what's needed.
  • One system per file (no mixing).

✗ Common pitfalls

  • Mixing require and import without config.
  • Forgetting ./ for a local module.
  • Forgetting the extension in ESM.

Key takeaways

  • Splitting code into MODULES (files with one responsibility, exported/imported) = fundamental for organisation and maintainability.
  • COMMONJS (historical, omnipresent): export module.exports, import require(). Synchronous, in millions of npm packages.
  • ES MODULES (standard, future): export export, import import; enabled via .mjs or "type":"module" in package.json.
  • Understand BOTH (require AND import coexist); ESM recommended for a new project. The PRINCIPLE matters more than the syntax.

48h challenge — apply tomorrow

Extract one module from your code (template above): module.exports + require('./...'), run with node, then convert it to export/import. 20 minutes is enough.

Section exit checklist
  • I can explain module, export and import.
  • I can tell require (CJS) apart from import (ESM).
  • I split out a module and imported it both ways.
Self-assessment 1-5: does each of my files have a clear responsibility?can I enable ESM and read CommonJS?

Frequently asked questions

CommonJS or ES Modules: which one to choose for my project?

For a new project, ESM is recommended: it's the official standard of the language (not a Node invention), consistent with front-end (same syntax in the browser), and the future direction. But CommonJS remains a valid choice (simplicity, compatibility with existing code) — and above all, you will read a lot of CommonJS (packages, tutorials, existing code). Why two systems? Node created CommonJS in 2009 because JavaScript had no module system then; the language standardised ES Modules in 2015 and Node adopted them without breaking existing code. Hence the coexistence to know.

How to organise my code into modules well, beyond syntax?

It's an architecture skill independent of require/import. Each module has one clear responsibility (users, database, routes, utilities) — no catch-all. Split by feature, export a clear, minimal interface (the public part, the rest stays private), name explicitly (userService.js, validation.js), avoid circular dependencies (A imports B which imports A) by organising hierarchically. Like Salim: three named modules beat one 500-line file.

Check your understanding

How to import a local module with CommonJS?

Additional resources (optional, to go further)

Level 1 examFresh, harder questions to test what you've learned