Skip to main content
In this chapter, we wrap up the main text of the book by exploring one of the most important code organization patterns in all of programming: the module. As we’ll see, modules are inherently built from what we’ve already covered: the payoff for your efforts in learning lexical scope and closure.
The central theme of this book has been that understanding and mastering scope and closure is key in properly structuring and organizing our code, especially the decisions on where to store information in variables.

Encapsulation and Least Exposure (POLE)

Encapsulation is often cited as a principle of object-oriented (OO) programming, but it’s more fundamental and broadly applicable than that. The goal of encapsulation is the bundling or co-location of information (data) and behavior (functions) that together serve a common purpose.

What Makes Encapsulation

Independent of any syntax or code mechanisms, the spirit of encapsulation can be realized in something as simple as using separate files to hold bits of the overall program with common purpose. Another key goal is the control of visibility of certain aspects of the encapsulated data and functionality. In JS, we most often implement visibility control through the mechanics of lexical scope.
The idea is to group alike program bits together, and selectively limit programmatic access to the parts we consider private details. What’s not considered private is then marked as public, accessible to the whole program.
The natural effect of this effort is better code organization. It’s easier to build and maintain software when we know where things are, with clear and obvious boundaries and connection points.

What Is a Module?

A module is a collection of related data and functions (often referred to as methods in this context), characterized by a division between hidden private details and public accessible details, usually called the “public API.” A module is also stateful: it maintains some information over time, along with functionality to access and update that information.

Namespaces (Stateless Grouping)

If you group a set of related functions together, without data, then you don’t really have the expected encapsulation a module implies. The better term for this grouping of stateless functions is a namespace:
// namespace, not module
var Utils = {
    cancelEvt(evt) {
        evt.preventDefault();
        evt.stopPropagation();
        evt.stopImmediatePropagation();
    },
    wait(ms) {
        return new Promise(function c(res){
            setTimeout(res,ms);
        });
    },
    isValidEmail(email) {
        return /[^@]+@[^@.]+\.[^@.]+/.test(email);
    }
};
Utils here is a useful collection of utilities, yet they’re all state-independent functions. Gathering functionality together is generally good practice, but that doesn’t make this a module. Rather, we’ve defined a Utils namespace.

Data Structures (Stateful Grouping)

Even if you bundle data and stateful functions together, if you’re not limiting the visibility of any of it, then you’re stopping short of the POLE aspect of encapsulation:
// data structure, not module
var Student = {
    records: [
        { id: 14, name: "Kyle", grade: 86 },
        { id: 73, name: "Suzy", grade: 87 },
        { id: 112, name: "Frank", grade: 75 },
        { id: 6, name: "Sarah", grade: 91 }
    ],
    getName(studentID) {
        var student = this.records.find(
            student => student.id == studentID
        );
        return student.name;
    }
};

Student.getName(73);
// Suzy
Since records is publicly accessible data, not hidden behind a public API, Student here isn’t really a module. It’s best to label this an instance of a data structure.

Modules (Stateful Access Control)

To embody the full spirit of the module pattern, we not only need grouping and state, but also access control through visibility (private vs. public). Let’s turn Student into a module. We’ll start with a form called the “classic module”:
var Student = (function defineStudent(){
    var records = [
        { id: 14, name: "Kyle", grade: 86 },
        { id: 73, name: "Suzy", grade: 87 },
        { id: 112, name: "Frank", grade: 75 },
        { id: 6, name: "Sarah", grade: 91 }
    ];

    var publicAPI = {
        getName
    };

    return publicAPI;

    // ************************

    function getName(studentID) {
        var student = records.find(
            student => student.id == studentID
        );
        return student.name;
    }
})();

Student.getName(73);   // Suzy
Student is now an instance of a module. It features a public API with a single method: getName(..). This method is able to access the private hidden records data.
The instance of the module is created by the defineStudent() IIFE being executed. This IIFE returns an object (named publicAPI) that has a property on it referencing the inner getName(..) function.From the outside, Student.getName(..) invokes this exposed inner function, which maintains access to the inner records variable via closure.

Classic Module Definition

So to clarify what makes something a classic module:
  1. There must be an outer scope, typically from a module factory function running at least once.
  2. The module’s inner scope must have at least one piece of hidden information that represents state for the module.
  3. The module must return on its public API a reference to at least one function that has closure over the hidden module state.

Module Factory (Multiple Instances)

If we want to define a module that supports multiple instances, we can slightly tweak the code:
// factory function, not singleton IIFE
function defineStudent() {
    var records = [
        { id: 14, name: "Kyle", grade: 86 },
        { id: 73, name: "Suzy", grade: 87 },
        { id: 112, name: "Frank", grade: 75 },
        { id: 6, name: "Sarah", grade: 91 }
    ];

    var publicAPI = {
        getName
    };

    return publicAPI;

    // ************************

    function getName(studentID) {
        var student = records.find(
            student => student.id == studentID
        );
        return student.name;
    }
}

var fullTime = defineStudent();
fullTime.getName(73);            // Suzy
Rather than specifying defineStudent() as an IIFE, we just define it as a normal standalone function, commonly referred to as a “module factory” function.

Node CommonJS Modules

Unlike the classic module format, where you could bundle the module factory or IIFE alongside any other code, CommonJS modules are file-based; one module per file. Let’s tweak our module example to adhere to that format:
module.exports.getName = getName;

// ************************

var records = [
    { id: 14, name: "Kyle", grade: 86 },
    { id: 73, name: "Suzy", grade: 87 },
    { id: 112, name: "Frank", grade: 75 },
    { id: 6, name: "Sarah", grade: 91 }
];

function getName(studentID) {
    var student = records.find(
        student => student.id == studentID
    );
    return student.name;
}
The records and getName identifiers are in the top-level scope of this module, but that’s not the global scope. Everything here is by default private to the module. To expose something on the public API of a CommonJS module, you add a property to the empty object provided as module.exports.
For style purposes, I like to put my “exports” at the top and my module implementation at the bottom. But these exports can be placed anywhere.
Some developers have the habit of replacing the default exports object. As such, I recommend against replacing the object. If you want to assign multiple exports at once, you can do this:
Object.assign(module.exports,{
   // .. exports ..
});
To include another module instance into your module/program, use Node’s require(..) method:
var Student = require("/path/to/student.js");

Student.getName(73);
// Suzy
Student now references the public API of our example module.
CommonJS modules behave as singleton instances, similar to the IIFE module definition style. No matter how many times you require(..) the same module, you just get additional references to the single shared module instance.
require(..) is an all-or-nothing mechanism. To effectively access only part of the API:
var getName = require("/path/to/student.js").getName;

// or alternately:
var { getName } = require("/path/to/student.js");

Modern ES Modules (ESM)

The ESM format shares several similarities with the CommonJS format. ESM is file-based, and module instances are singletons, with everything private by default. One notable difference is that ESM files are assumed to be strict-mode, without needing a "use strict" pragma at the top. Instead of module.exports in CommonJS, ESM uses an export keyword to expose something on the public API of the module. The import keyword replaces the require(..) statement:
export { getName };

// ************************

var records = [
    { id: 14, name: "Kyle", grade: 86 },
    { id: 73, name: "Suzy", grade: 87 },
    { id: 112, name: "Frank", grade: 75 },
    { id: 6, name: "Sarah", grade: 91 }
];

function getName(studentID) {
    var student = records.find(
        student => student.id == studentID
    );
    return student.name;
}
The only change here is the export { getName } statement. export statements can appear anywhere throughout the file, though export must be at the top-level scope.

ESM Export Variations

ESM offers variation on how the export statements can be specified:
export function getName(studentID) {
    // ..
}
Even though export appears before the function keyword here, this is still a function declaration that also happens to be exported. The getName identifier is function hoisted, so it’s available throughout the whole scope of the module. Another allowed variation:
export default function getName(studentID) {
    // ..
}
This is a “default export,” which has different semantics from other exports. Non-default exports are referred to as “named exports.”

ESM Import Variations

The import keyword has a number of variations in syntax. The first is referred to as “named import”:
import { getName } from "/path/to/students.js";

getName(73);   // Suzy
This form imports only the specifically named public API members from a module, and adds those identifiers to the top-level scope of the current module. Multiple API members can be listed inside the { .. } set, separated with commas. A named import can also be renamed with the as keyword:
import { getName as getStudentName }
   from "/path/to/students.js";

getStudentName(73);   // Suzy
If getName is a “default export” of the module:
import getName from "/path/to/students.js";

getName(73);   // Suzy
The only difference here is dropping the { } around the import binding. By contrast, the other major variation on import is called “namespace import”:
import * as Student from "/path/to/students.js";

Student.getName(73);   // Suzy
The * imports everything exported to the API, default and named, and stores it all under the single namespace identifier as specified. This approach most closely matches the form of classic modules.

Exit Scope

Whether you use the classic module format (browser or Node), CommonJS format (in Node), or ESM format (browser or Node), modules are one of the most effective ways to structure and organize your program’s functionality and data.
The module pattern is the conclusion of our journey in this book of learning how we can use the rules of lexical scope to place variables and functions in proper locations. POLE is the defensive private by default posture we always take.And underneath modules, the magic of how all our module state is maintained is closures leveraging the lexical scope system.
That’s it for the main text. Congratulations on quite a journey so far! When you’re comfortable and ready, check out the appendices, which dig deeper into some of the corners of these topics.

Build docs developers (and LLMs) love