Skip to content
Node.js nd patterns 5 min read

The Module Pattern

The module pattern is the oldest and most foundational pattern in JavaScript: it bundles related state and behaviour into a single unit, keeps the internals private, and exposes only a deliberate public API. Before JavaScript had a real module system, developers leaned on closures and immediately-invoked function expressions (IIFEs) to achieve this. Node.js later formalised the idea — first with CommonJS, then with ECMAScript modules — so that every file is itself a module with private scope. Understanding the underlying mechanics makes you a better designer of both files and runtime objects.

The problem: a shared global scope

In a browser without modules, every var and function declared at the top level lands on the global object. Two scripts that both define a counter will silently clobber each other, and anything can reach in and mutate your internals. The module pattern solves this by creating a private scope and returning only what callers are meant to touch.

Closures and the IIFE

The mechanism is the closure: a function retains access to the variables in the scope where it was defined, even after that scope has finished executing. Wrap your state in a function, run it immediately, and return an object whose methods close over that state. The state lives on, but nothing outside can reach it directly.

const counter = (function () {
  // Private — not accessible from outside this function.
  let count = 0;

  function validate(step) {
    if (!Number.isInteger(step)) throw new TypeError("step must be an integer");
  }

  // Public API — the only handles the caller gets.
  return {
    increment(step = 1) {
      validate(step);
      count += step;
      return count;
    },
    decrement(step = 1) {
      validate(step);
      count -= step;
      return count;
    },
    get value() {
      return count;
    },
  };
})();

console.log(counter.increment());   // 1
console.log(counter.increment(4));  // 5
console.log(counter.decrement());   // 4
console.log(counter.value);         // 4
console.log(counter.count);         // undefined — truly private

Output:

1
5
4
4
undefined

The parentheses wrapping the function turn a declaration into an expression, and the trailing () invokes it on the spot. count and validate never escape; the returned object is the whole public surface.

The revealing module pattern

A popular refinement is the revealing module pattern. Instead of building the return object inline, you define every function as a private local and then reveal a curated subset by name. This keeps the public/private boundary in one easy-to-read place and lets internal functions call each other by their local names.

const userStore = (function () {
  const users = new Map();

  function add(user) {
    if (!user.id) throw new Error("user.id is required");
    users.set(user.id, { ...user });
  }

  function find(id) {
    return users.get(id) ?? null;
  }

  function count() {
    return users.size;
  }

  // Reveal only what's public; `users` stays hidden.
  return { add, find, count };
})();

userStore.add({ id: "u1", name: "Ada" });
userStore.add({ id: "u2", name: "Linus" });
console.log(userStore.count());        // 2
console.log(userStore.find("u1"));     // { id: 'u1', name: 'Ada' }

Gotcha: with the revealing pattern, if a public method is reassigned by a consumer, internal callers still reference the original private function. That is usually desirable, but be aware the revealed names and the private names can drift apart if you reassign them.

How Node.js formalises the pattern

Node never needed the IIFE trick for file scoping, because every file is already a module with its own scope. Under the hood, CommonJS wraps each file in a function so that module, exports, require, __dirname, and __filename are local — exactly the closure technique, applied automatically.

// logger.js (CommonJS)
let lines = 0;                       // private module state

function log(message) {
  lines += 1;
  console.log(`[${lines}] ${message}`);
}

module.exports = { log };            // public API

ES modules (the modern default) make the boundary explicit with export and import. Top-level bindings are private to the file unless exported, and bindings are live — importers see updates to exported values.

// logger.mjs (ES module)
let lines = 0;                       // private to this module

export function log(message) {
  lines += 1;
  console.log(`[${lines}] ${message}`);
}

// app.mjs
import { log } from "node:url";       // core modules use the node: prefix
import { log as appLog } from "./logger.mjs";
appLog("started");

Comparing the approaches

AspectIIFE / revealing moduleCommonJS (require)ES modules (import)
ScopingManual (closure)Per-file, automaticPer-file, automatic
PrivacyClosure variablesUnexported localsUnexported locals
LoadingSynchronous, inlineSynchronousStatic + dynamic import()
BindingsSnapshot in objectCopied valueLive binding
Best forSingle runtime objectLegacy / scriptsNew code (Node 20/22)

A modern singleton-style module

Because a module body runs once and is cached, exporting a configured object gives you encapsulated, shared state for free — no IIFE required.

// config.mjs
const settings = Object.freeze({
  port: Number(process.env.PORT) || 3000,
  env: process.env.NODE_ENV ?? "development",
});

export function get(key) {
  return settings[key];
}

Every importer of config.mjs shares the same frozen settings, while the object itself stays inaccessible outside the file.

Best Practices

  • Default to ES modules in new Node code; reach for the IIFE pattern only when you need an encapsulated object inside a single file.
  • Expose the smallest possible public API — reveal functions and getters, keep raw state private.
  • Prefer getters over public fields so consumers read state without being able to corrupt it.
  • Use the node: prefix for core modules (node:fs, node:crypto) to disambiguate them from npm packages.
  • Remember ES module bindings are live, so importers see later mutations to exported values — freeze or copy when you need a stable snapshot.
  • Avoid leaking internals by accident: never return references to private mutable structures; return copies or read-only views.
Last updated June 14, 2026
Was this helpful?