JS Modules: The Backbone of Code
As JavaScript applications grow larger and more complex, it becomes increasingly important to structure code in a way that’s clean, organized, and easy to maintain. That’s where modules come in. In this post, we’ll explore what modules are, how modern JavaScript handles them using export and import, and how Node.js leverages the older CommonJS system. Whether you’re new to modules or just need a refresher, you’re in the right place. Why Modules Matter Back in the early days of the web, JavaScript was used for simple page interactions—think form validation or small effects. Everything lived in one script tag, and that was perfectly fine. But as applications became more sophisticated, developers needed a way to: Reuse code across files Keep code maintainable and clean Avoid polluting the global scope That’s where modules come in. Modules let you encapsulate code—functions, variables, classes—and export them for use in other files. A Quick History of JavaScript Modules Before JavaScript had native module support, developers came up with a few community-driven solutions: AMD (Asynchronous Module Definition): Used primarily in the browser with libraries like require.js. CommonJS: Designed for server-side JavaScript, specifically Node.js. UMD (Universal Module Definition): A combination of AMD and CommonJS, aiming for broader compatibility. These were clever workarounds, but they weren’t standardized. That changed in 2015 with the introduction of native ES Modules (ESM) in ECMAScript 6 (also known as ES2015). Now, the import and export syntax is widely supported across all modern environments, including Node.js. What is a Module? In simple terms: a module is just a JavaScript file. One file = one module. By using export and import, you can make parts of one file available to another. Here’s an example: // sayHi.js export function sayHi(user) { alert(`Hello, ${user}!`); } Now in another file: // main.js import { sayHi } from './sayHi.js'; sayHi('John'); // Hello, John! Notice the .js extension is required when using ES Modules in the browser or modern Node.js setups. Ways to Export There are two main ways to export values in JavaScript. 1. Export in-line This method puts export directly before the declaration: export let months = ['Jan', 'Feb', 'Mar']; export const MODULES_YEAR = 2015; export class User { constructor(name) { this.name = name; } } 2. Export after declaration You can declare values first, and export them later: function sayHi(user) { alert(`Hello, ${user}!`); } function sayBye(user) { alert(`Bye, ${user}!`); } export { sayHi, sayBye }; This approach is useful when you want to group your exports at the bottom of the file. Importing Modules You’ve already seen this basic pattern: import { sayHi, sayBye } from './say.js'; sayHi('Alice'); sayBye('Bob'); Import Everything You can also import everything under a namespace: import * as say from './say.js'; say.sayHi('Alice'); say.sayBye('Bob'); While this is convenient, it’s sometimes better to be explicit. Listing what you import gives a clearer view of which functions you’re using and keeps names shorter. CommonJS: The Node.js Module System Before ES Modules were officially supported, Node.js used the CommonJS module format. If you’ve seen require() and module.exports, that’s CommonJS in action. Here’s a simple CommonJS example: // add.js function add(a, b) { return a + b; } module.exports = add; And how to use it: // index.js const add = require('./add'); console.log(add(4, 5)); // 9 What’s Going On Under the Hood? Node wraps every module in a function like this: (function (exports, require, module, __filename, __dirname) { // your code here }); This means each module has its own scope and access to these special variables. It prevents variables from leaking into the global scope and lets you use require, exports, and module seamlessly. The require() Caching System Another neat feature: Node.js caches modules. When you require() a file, Node loads and evaluates it just once. Subsequent calls return the cached version. This makes your modules singleton-like, sharing the same state across the app. How Node Finds Modules When you require('something'), Node checks: Is it a built-in module like fs or path? Is it a relative path like ./utils or ../lib? Otherwise, it looks in node_modules/, walking up the directory tree until it finds the module or reaches the root. Writing Clean, Maintainable Modules When creating modules, aim for: High cohesion: Each module should do one thing well. Loose coupling: Modules shouldn’t rely on global state. Pass data as parameters. Explicit exports: Export only what you need, and name it clearly. A common structure: // db.js

As JavaScript applications grow larger and more complex, it becomes increasingly important to structure code in a way that’s clean, organized, and easy to maintain.
That’s where modules come in.
In this post, we’ll explore what modules are, how modern JavaScript handles them using export
and import
, and how Node.js leverages the older CommonJS system.
Whether you’re new to modules or just need a refresher, you’re in the right place.
Why Modules Matter
Back in the early days of the web, JavaScript was used for simple page interactions—think form validation or small effects.
Everything lived in one script tag, and that was perfectly fine.
But as applications became more sophisticated, developers needed a way to:
- Reuse code across files
- Keep code maintainable and clean
- Avoid polluting the global scope
That’s where modules come in. Modules let you encapsulate code—functions, variables, classes—and export them for use in other files.
A Quick History of JavaScript Modules
Before JavaScript had native module support, developers came up with a few community-driven solutions:
-
AMD (Asynchronous Module Definition): Used primarily in the browser with libraries like
require.js
. - CommonJS: Designed for server-side JavaScript, specifically Node.js.
- UMD (Universal Module Definition): A combination of AMD and CommonJS, aiming for broader compatibility.
These were clever workarounds, but they weren’t standardized.
That changed in 2015 with the introduction of native ES Modules (ESM) in ECMAScript 6 (also known as ES2015).
Now, the import
and export
syntax is widely supported across all modern environments, including Node.js.
What is a Module?
In simple terms: a module is just a JavaScript file.
One file = one module.
By using export
and import
, you can make parts of one file available to another. Here’s an example:
// sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
Now in another file:
// main.js
import { sayHi } from './sayHi.js';
sayHi('John'); // Hello, John!
Notice the .js
extension is required when using ES Modules in the browser or modern Node.js setups.
Ways to Export
There are two main ways to export values in JavaScript.
1. Export in-line
This method puts export
directly before the declaration:
export let months = ['Jan', 'Feb', 'Mar'];
export const MODULES_YEAR = 2015;
export class User {
constructor(name) {
this.name = name;
}
}
2. Export after declaration
You can declare values first, and export them later:
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
export { sayHi, sayBye };
This approach is useful when you want to group your exports at the bottom of the file.
Importing Modules
You’ve already seen this basic pattern:
import { sayHi, sayBye } from './say.js';
sayHi('Alice');
sayBye('Bob');
Import Everything
You can also import everything under a namespace:
import * as say from './say.js';
say.sayHi('Alice');
say.sayBye('Bob');
While this is convenient, it’s sometimes better to be explicit. Listing what you import gives a clearer view of which functions you’re using and keeps names shorter.
CommonJS: The Node.js Module System
Before ES Modules were officially supported, Node.js used the CommonJS module format. If you’ve seen require()
and module.exports
, that’s CommonJS in action.
Here’s a simple CommonJS example:
// add.js
function add(a, b) {
return a + b;
}
module.exports = add;
And how to use it:
// index.js
const add = require('./add');
console.log(add(4, 5)); // 9
What’s Going On Under the Hood?
Node wraps every module in a function like this:
(function (exports, require, module, __filename, __dirname) {
// your code here
});
This means each module has its own scope and access to these special variables.
It prevents variables from leaking into the global scope and lets you use require
, exports
, and module
seamlessly.
The require()
Caching System
Another neat feature: Node.js caches modules. When you require()
a file, Node loads and evaluates it just once.
Subsequent calls return the cached version.
This makes your modules singleton-like, sharing the same state across the app.
How Node Finds Modules
When you require('something')
, Node checks:
- Is it a built-in module like
fs
orpath
? - Is it a relative path like
./utils
or../lib
? - Otherwise, it looks in
node_modules/
, walking up the directory tree until it finds the module or reaches the root.
Writing Clean, Maintainable Modules
When creating modules, aim for:
- High cohesion: Each module should do one thing well.
- Loose coupling: Modules shouldn’t rely on global state. Pass data as parameters.
- Explicit exports: Export only what you need, and name it clearly.
A common structure:
// db.js
'use strict';
const CONNECTION_LIMIT = 10;
function connect() {
// connect logic here
}
module.exports = {
CONNECTION_LIMIT,
connect
};
Which Module System Should You Use?
- For modern browsers and Node.js (v14+), ES Modules (
import/export
) are preferred. - For older Node.js projects, or if you're using tools like
require.cache
, stick with CommonJS (require/module.exports
).
Node.js is gradually embracing ES Modules more natively, but for compatibility, both systems are still in use today.
Final Thoughts
Modules are one of the best things to happen to JavaScript.
Whether you’re using the modern ES6 syntax or sticking with CommonJS for Node.js, understanding how modules work helps you build better, more modular applications.
Next time your project gets a little too large for comfort, consider breaking it into smaller files with clearly defined interfaces.
Your future self (and your teammates) will thank you.
I’ve been actively working on a super-convenient tool called LiveAPI.
LiveAPI helps you get all your backend APIs documented in a few minutes
With LiveAPI, you can quickly generate interactive API documentation that allows users to execute APIs directly from the browser.
If you’re tired of manually creating docs for your APIs, this tool might just make your life easier.