Notifications

No notifications

/Phase 1

Modules & require

Node.js Module System

Modules let you split code into separate files and import what you need. Node.js supports two module systems:

CommonJS (CJS) — The Original

// math.js — Exporting
module.exports = { add, subtract };
// OR
exports.add = (a, b) => a + b;

// app.js — Importing const math = require('./math'); math.add(2, 3);

ES Modules (ESM) — The Modern Way

// math.mjs — Exporting
export const add = (a, b) => a + b;
export default class Calculator { }

// app.mjs — Importing import Calculator, { add } from './math.mjs';

Key Differences

FeatureCommonJSES Modules
Syntaxrequire() / module.exportsimport / export
LoadingSynchronousAsynchronous
File ext.js.mjs or "type": "module"
Top-level await
Used inNode.js (default)Browsers + Node.js

On this page

Detailed Theory

What a Module Is

A module is just one file. Splitting your app into small files keeps it understandable and makes pieces reusable. Node loads each file once, runs it, and gives other files a way to grab whatever it exposes.

Node supports two module systems side-by-side:

  • CommonJS (CJS) — the original Node way, uses require / module.exports.
  • ES Modules (ESM) — the modern, web-standard way, uses import / export.
Both still appear daily in real codebases.

CommonJS in 60 Seconds

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

module.exports = { add, sub };

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

  • require('./math') — leading ./ means "local file".
  • require('express') — no leading dot means "look in node_modules".

ES Modules in 60 Seconds

// math.mjs (or any .js file in a project with "type": "module")
export function add(a, b) { return a + b; }
export function sub(a, b) { return a - b; }
export default function mul(a, b) { return a * b; }

// app.mjs
import mul, { add, sub } from './math.mjs';

Key rule: ESM file imports usually need the full filename with extension (./math.mjs), CJS does not.

Picking Which to Use

  • New project, no constraints → ESM ("type": "module" in package.json).
  • Older codebase or a tutorial that says requireCommonJS is still completely fine.
  • Some libraries are ESM-only now (e.g. recent versions of node-fetch, chalk). If require complains, your project probably needs to switch to ESM.

Beginner Mistakes to Skip

1. Mixing require and import in the same file. Pick one per file — Node won't let you mix. 2. Forgetting ./ for local files. require('math') looks in node_modules; require('./math') looks next door. 3. module.exports = something *and* exports.x = .... The first replaces the export object entirely, wiping out the second. 4. Circular imports. a.js requires b.js which requires a.js. Half-loaded modules give you confusing undefineds. Refactor to a third file holding the shared piece. 5. Editing a file inside node_modules/. Your changes vanish on the next npm install.

Intermediate: How require Actually Works

When you call require('./myModule'), Node:

1. Resolves the path (core module? local file? node_modules?) 2. Loads the file contents from disk 3. Wraps the code in a function (the "module wrapper") 4. Executes the wrapped code 5. Caches the result — subsequent requires return the cached export

// Every CJS module is invisibly wrapped in:
(function (exports, require, module, __filename, __dirname) {
  // your code lives here
});

That wrapper is why __dirname, __filename, require, module, and exports are all magically available.

Intermediate: Module Resolution Order

require('express')
  1. Built-in core module? (fs, path, http…)  → use that
  2. Starts with './' or '/' or '../'?         → load as file/folder
  3. Otherwise, walk up looking in node_modules:
     ./node_modules/express
     ../node_modules/express
     ../../node_modules/express
     … until the filesystem root

For folders, Node looks for index.js or whatever the folder's own package.json lists in "main".

Intermediate: The Module Cache (Singleton Pattern)

Because Node caches modules, anything you put at the top level runs once. That makes modules a natural place for singletons like a database connection:

// db.js
let connection = null;
function connect() { if (!connection) connection = createConn(); return connection; }
module.exports = { connect };

Every file that does require('./db') gets the same exported object — and so the same connection.

Intermediate: Factory Exports

Export a function so callers can configure each instance:

// logger.js
module.exports = (prefix) => ({
  log:   (m) => console.log([${prefix}] ${m}),
  error: (m) => console.error([${prefix}] ERROR ${m}),
});

// usage const logger = require('./logger')('auth'); logger.log('signed in');

Intermediate: package.json and "type"

{
  "name": "my-app",
  "type": "module",          // "module" → ESM, "commonjs" (default) → CJS
  "main": "src/index.js",    // entry point when others import this package
  "exports": {                // modern alternative — finer control
    ".": "./src/index.js",
    "./utils": "./src/utils.js"
  }
}

exports lets a library hide internal files and expose only specific subpaths. Required for ESM packages that want to remain importable from CJS too.

Advanced: Dynamic & Conditional Imports

// CJS — dynamic require (runtime path)
const mod = require(process.env.PLUGIN);

// ESM — dynamic import returns a Promise const mod = await import('./plugin.mjs');

Use for plugin systems, lazy-loading rarely-used heavy deps, or loading ESM from CJS code.

Advanced: node: Prefix for Built-ins

import fs from 'node:fs/promises';
const path = require('node:path');

The node: prefix makes it explicit you mean the built-in (no chance of a naming clash with an npm package). Recommended for new code.

Advanced: Subpath Imports & Path Aliases

Tired of require('../../../lib/db')? Define internal aliases in package.json:

{
  "imports": {
    "#db": "./src/lib/db.js",
    "#utils/*": "./src/utils/*.js"
  }
}

import db from '#db';
import { slugify } from '#utils/strings';

No bundler required — Node understands # imports natively.

Advanced: Top-Level await (ESM only)

// config.mjs
const data = await fetch('https://example.com/config').then(r => r.json());
export default data;

A huge ergonomic win — no IIFE wrappers needed for setup code. Not available in CommonJS.

Practice Path

1. Create a math.js with add/sub, export them, and use them from index.js (CJS). 2. Convert the same project to ESM ("type": "module", change requireimport). 3. Build a tiny logger factory and use it from two different files; confirm both calls share the same prefix you passed. 4. Add an #config subpath import in package.json and import it from a deeply-nested file.