Module F-2·20 min read

require() vs import/export, how Node resolves modules, built-in core modules, and the circular dependency trap every beginner falls into.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module F-2 — The Module System: CommonJS and ESM

What this module covers: Every file you write in Node.js is a module. Understanding how modules work — how they import and export code, how Node.js finds the files you reference, and why two different systems (require and import) exist side by side — is foundational to everything else. This module explains CommonJS, ES Modules, the Node.js module resolution algorithm, the built-in core modules, and the circular dependency problem that catches every developer at least once.


Why Modules Exist

Imagine writing an entire application in a single file. At a hundred lines it's manageable. At a thousand it's painful. At ten thousand it's unmaintainable. You cannot test pieces of it in isolation. You cannot reuse logic across projects. Every variable is global and any part of the code can accidentally overwrite anything else.

Modules solve this by giving each file its own scope. Variables defined in utils.js don't exist in server.js unless utils.js explicitly exports them and server.js explicitly imports them. This is the entire point: controlled, explicit sharing of code between files.

Node.js has had two module systems across its history: CommonJS (the original, still widely used) and ES Modules (the modern standard, aligned with the browser). You will encounter both in the wild. You need to understand both.


CommonJS: The Original System

CommonJS was Node.js's module system from day one in 2009. It uses require() to load modules and module.exports (or exports) to expose code from a file.

Exporting from a file

javascript

You can also export a single value:

javascript

Or add to exports incrementally:

javascript

Module Caching: require() Only Runs a File Once

Here is the single biggest gap in most beginners' mental model: require() (and import) caches modules by their resolved file path. The first time you require('./db'), Node.js runs db.js top to bottom and caches whatever it exported. Every subsequent require('./db') — from any other file, anywhere in your project — returns that same cached object. It does not re-run db.js.

javascript
javascript

'Connecting to the database...' prints only once, no matter how many files require('./db'). This is not an optimisation detail you can ignore — it's the entire mechanism behind patterns you'll rely on constantly later in this course: a singleton database client that every file shares, a single shared EventEmitter acting as an event bus, a config object loaded once and reused everywhere. If module caching didn't work this way, each require('./db') would open a brand-new, independent database connection.

Importing with require()

javascript

A few things to notice:

  • The path './math' is relative to the current file. The ./ means "same directory". Without it, Node.js looks in node_modules (covered shortly).
  • The .js extension is optional — require('./math') and require('./math.js') are equivalent.
  • require() is synchronous — it blocks until the file is fully loaded and executed. This is fine at startup but you should never call require() inside a hot code path (inside a function that runs on every request).

The module.exports vs exports distinction

This trips up beginners frequently. Both module.exports and exports start out pointing to the same object. But if you reassign module.exports, you replace the export entirely:

javascript

Rule of thumb: Use module.exports = ... when you want to export a single thing (a function, a class, an object). Use exports.name = ... when you want to add properties incrementally. Never reassign exports directly.

Detecting Direct Execution: require.main === module

A common need: a file should behave differently depending on whether it was run directly (node script.js) or loaded by another file via require(). The standard CommonJS idiom for this is require.main === module:

javascript

require.main is set to the module object of whichever file was originally executed by the node command. Comparing it to the current file's own module object tells you whether you're the entry point or just an import. You'll see this idiom constantly in CLI tools and scripts that are also meant to be reusable as libraries.


ES Modules: The Modern Standard

ES Modules (ESM) were standardised in ES2015. Node.js support arrived gradually — experimental support (behind a flag, .mjs extension) landed in Node 8.5 back in 2017, and unflagged, stable support arrived incrementally across Node 12–13. They use import and export syntax and are the module system used in browsers, Deno, and all modern JavaScript tooling.

Exporting from a file

javascript

Importing

javascript

You can also import everything under a namespace:

javascript

Key differences from CommonJS

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.