A module is a file with an explicit public boundary. It exports names that other files may import, and keeps the rest local. Modules reduce accidental global variables and make dependencies visible. They do not automatically make a program secure or remove circular dependencies; the dependency graph still needs design.
A complete two-file example
Save these files in the same directory:
export function totalMinutes(values) {
if (!values.every(value => Number.isInteger(value) && value >= 0)) {
throw new Error("Durations must be non-negative whole minutes.");
}
return values.reduce((sum, value) => sum + value, 0);
}
import { totalMinutes } from "./durations.mjs";
console.log(totalMinutes([25, 15]));
node main.mjs prints 40. Named imports must match exported names, although an import can be renamed with as. A default export supplies one default value per module; it is a different syntax, not a more powerful form of export. Prefer consistent named exports while learning. A namespace import groups a module's exports under one name.
Relative ESM paths in Node normally need the filename extension. Browser imports are URLs; a bare package name does not work there without an import map or a build tool that resolves it. Installing a package with npm only places files in a project and records dependency information. It does not instruct every browser how to resolve an import.
Understand the project files
A package.json describes a package, its scripts and dependencies. A package-lock.json records a resolved dependency tree; commit it for an application. Use npm ci in a clean automated build when the lockfile is present and synchronized. A project using .js ESM files can set "type": "module"; .mjs explicitly selects ESM regardless of that setting. CommonJS uses a different module system with require and module.exports. Do not casually mix both styles in one introductory example.
Build tools can bundle modules and transform syntax, but that does not provide a missing runtime API. A formatter handles layout, a linter flags suspicious patterns, a type checker examines types, and tests exercise behavior. These tools catch different problems. Start with the smallest toolchain you understand rather than adding a framework to fix an import typo.
Static imports describe dependencies before normal module execution. Dynamic import() returns a promise and is useful for optional or delayed functionality. Repeated imports generally reuse the same module instance within the same environment and resolution identity; changing a shared exported object can therefore affect multiple importers. Exported bindings are live bindings, and an importer cannot simply reassign another module's exported variable.
Avoid brittle module boundaries
Keep domain calculations independent of DOM access or process startup. Importing a utility should not unexpectedly open a network listener. Avoid circular dependencies: reading an export before its initialization can fail even when the same code worked in a different loading order. A clean dependency direction is easier to maintain than a collection of barrel files importing one another.
Exercise: add averageMinutes(values) to the duration module. Decide whether an empty list returns zero or throws, document the choice, and test it from a separate entry file.
Solution guidance: reuse the validated total calculation and divide only after checking the length. Keep the empty-input rule consistent. Run the entry file from the terminal; importing an unused name is not proof that the function works.
References: ES modules, npm lockfiles.
Previous: this, Prototypes and Classes. Next: Errors, Validation and Reliable Boundaries.

