Skip to content

Node.js Modules

Modules are how Node.js keeps large codebases manageable, each file gets its own private scope, and you decide exactly what to expose. This lesson introduces the module system before the following lessons go deep on CommonJS and ES modules individually.

Why Node.js Has Modules

Without a module system, every file's variables would either be completely isolated or all dumped into one shared global scope, both are unworkable for real applications. Node.js solves this by treating every file as a module: it runs in its own scope, and you explicitly export values from it and import/require those values elsewhere.

Node.js currently supports two module systems side by side: CommonJS (require/module.exports), the original system Node.js shipped with, and ES modules (import/export), the standard JavaScript module system also used in browsers. Both are covered in their own lessons right after this one.

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

module.exports = { add };

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

math.js explicitly exports add; nothing else defined in that file is visible from app.js unless it's also exported.

Two Module Systems, One Runtime

CommonJS:    const x = require('./file.js');   module.exports = x;
ES Modules:  import x from './file.js';        export default x;
  • CommonJS is Node's original module system and is synchronous by design.
  • ES modules (ESM) are the JavaScript standard, shared with browsers, and load asynchronously.
  • A project's package.json "type" field (or file extension .cjs/.mjs) determines which system a file uses.
  • You generally should not mix require and import in the same file.

Node.js Modules Cheatsheet

How the two module systems compare at a glance.

Aspect CommonJS ES Modules
Import const x = require('./x') import x from './x.js'
Export module.exports = x export default x
Named export exports.x = x export const x = 1
Loading Synchronous Asynchronous (spec-level)
File hint .cjs or "type": "commonjs" .mjs or "type": "module"
Current file dir __dirname available Use import.meta.url instead

Every Module Has Its Own Scope

Variables and functions declared at the top level of a file are private to that module by default, they do not leak into other files the way top-level var declarations might in old-style browser scripts loaded via multiple <script> tags.

  • Only what you explicitly export is visible outside the file.
  • This prevents accidental name collisions between unrelated files.
  • Node.js caches modules after the first load, requiring/importing the same file twice returns the same instance.

How Node.js Resolves Module Paths

When you require('./utils') or import './utils.js', Node.js has a resolution algorithm: relative paths (./, ../) resolve against the current file, bare names (express) resolve by walking up node_modules folders, and built-in modules (fs, path) resolve to Node's own internals, optionally prefixed with node:.

require('./local-file.js');   // relative path, your own code
require('express');           // package from node_modules
require('node:fs');           // Node.js built-in module

Common Mistakes

  • Expecting a variable declared in one file to be automatically available in another without exporting it.
  • Mixing require() and import syntax in the same file, which usually causes a syntax or runtime error.
  • Forgetting the ./ prefix on relative imports, which makes Node.js look in node_modules instead.
  • Not realizing repeated require() calls for the same file return a cached, shared module instance.

Key Takeaways

  • Every Node.js file is its own module with a private scope by default.
  • Node.js supports two module systems: CommonJS and ES modules.
  • Module resolution differs for relative paths, package names, and built-in modules.
  • Modules are cached after their first load and reused on subsequent imports.

Pro Tip

When debugging "why is this undefined", check the export first. A huge share of module-related bugs are simply forgetting to export/module.exports a value you assumed was already public.