As JavaScript applications grow, keeping all code inside a single file becomes difficult.
Imagine having everything inside:
app.js
including:
User Logic
Product Logic
Authentication
API Requests
Validation
Utility Functions
UI Logic
The file can quickly become thousands of lines long.
Modules solve this problem by allowing us to split our code into smaller files.
Benefits:
A module is a JavaScript file containing code that can be exported and used by another file.
Think of it as:
File A
↓
Export Something
↓
File B
↓
Import It
↓
Use It
Example:
math.js
export const add =
(a, b) => a + b;
app.js
import { add }
from "./math.js";
console.log(
add(10, 20)
);
Output:
30
Here:
math.js
↓
Exports add()
↓
app.js
↓
Imports add()
↓
Uses add()
Without modules, we might have:
app.js
│
├── User Code
├── Product Code
├── Authentication
├── API Code
├── Validation Code
├── Formatting Code
├── Cart Code
└── Utility Code
Everything exists inside one large file.
This becomes difficult to:
Read
Debug
Maintain
Test
Reuse
With modules:
src/
│
├── users/
│ └── user.js
│
├── products/
│ └── product.js
│
├── api/
│ └── api.js
│
├── utils/
│ └── helpers.js
│
└── app.js
Each file has a specific responsibility.
Suppose we need mathematical functions.
math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
Now another file can use them.
app.js
import {
add,
subtract
} from "./math.js";
console.log(
add(10, 5)
);
console.log(
subtract(10, 5)
);
Output:
15
5
Two important module systems you will encounter are:
They use different syntax.
CommonJS
↓
require()
module.exports
ES Modules
↓
import
export
CommonJS, often called CJS, is the traditional module system associated with Node.js.
It uses:
require()
for importing and:
module.exports
for exporting.
math.js
function add(a, b) {
return a + b;
}
module.exports = add;
Here:
module.exports
↓
Makes add() available
outside math.js
app.js
const add =
require("./math");
console.log(
add(10, 20)
);
Output:
30
Flow:
math.js
↓
module.exports
↓
require()
↓
app.js
We can export an object containing multiple values.
user.js
const name = "Samir";
const age = 21;
function greet() {
console.log(
`Hello ${name}`
);
}
module.exports = {
name,
age,
greet
};
const user =
require("./user");
console.log(
user.name
);
console.log(
user.age
);
user.greet();
Output:
Samir
21
Hello Samir
Instead of:
const user =
require("./user");
console.log(
user.name
);
we can destructure the exported object:
const {
name,
age,
greet
} = require("./user");
Then:
console.log(name);
console.log(age);
greet();
Output:
Samir
21
Hello Samir
CommonJS is commonly associated with:
Node.js
Legacy Node.js Projects
Older Packages
Server-Side JavaScript
Its main syntax is:
const something =
require("./file");
and:
module.exports =
something;
ES Modules, commonly called ESM, are JavaScript's standardized module system.
They were introduced as part of ES6 / ECMAScript 2015.
ES Modules use:
export
and:
import
Example:
math.js
export function add(a, b) {
return a + b;
}
app.js
import { add }
from "./math.js";
ES Modules are widely used in modern JavaScript development because they provide:
Standard JavaScript Syntax
Browser Support
Node.js Support
Static Analysis
Tree Shaking
Code Splitting Support
Better Tooling
Modern tools and frameworks commonly work with ESM, including:
React
Next.js
Vite
Node.js
TypeScript
A named export exports something using its name.
Example:
math.js
export function add(a, b) {
return a + b;
}
Import:
import { add }
from "./math.js";
Usage:
console.log(
add(10, 20)
);
Output:
30
The { } are important.
import { add }
from "./math.js";
because add is a named export.
A module can contain multiple named exports.
math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
Import them:
import {
add,
subtract,
multiply
} from "./math.js";
Usage:
console.log(
add(10, 5)
);
console.log(
subtract(10, 5)
);
console.log(
multiply(10, 5)
);
Output:
15
5
50
Variables can also be exported.
user.js
export const name =
"Samir";
export const age =
21;
export const country =
"Nepal";
Import:
import {
name,
age,
country
} from "./user.js";
Usage:
console.log(name);
console.log(age);
console.log(country);
Output:
Samir
21
Nepal
Exports do not always need to appear directly before a variable or function.
We can first create values:
const name = "Samir";
const age = 21;
function greet() {
console.log(
`Hello ${name}`
);
}
Then export them:
export {
name,
age,
greet
};
Both approaches are valid.
A module can have one default export.
Example:
greet.js
export default function greet() {
console.log("Hello");
}
Import:
import greet
from "./greet.js";
Usage:
greet();
Output:
Hello
Notice that default imports do not use:
{ }
user.js
const user = {
name: "Samir",
age: 21,
country: "Nepal"
};
export default user;
Import:
import user
from "./user.js";
Usage:
console.log(
user.name
);
Output:
Samir
Export:
export const name =
"Samir";
Import:
import { name }
from "./user.js";
Export:
const user = {
name: "Samir"
};
export default user;
Import:
import user
from "./user.js";
Main difference:
Named Export
↓
Uses { }
Default Export
↓
Does Not Use { }
A module can have many named exports:
export const add = () => {};
export const subtract = () => {};
export const multiply = () => {};
But only one default export:
export default function calculate() {
}
This would be invalid:
export default function add() {}
export default function subtract() {}
because a module cannot have multiple default exports.
A file can contain:
One Default Export
+
Multiple Named Exports
Example:
user.js
const user = {
name: "Samir"
};
export const age = 21;
export const country =
"Nepal";
export default user;
Import:
import user, {
age,
country
} from "./user.js";
Usage:
console.log(
user.name
);
console.log(age);
console.log(country);
Named imports can be renamed using:
as
Export:
export const name =
"Samir";
Import:
import {
name as userName
} from "./user.js";
Usage:
console.log(
userName
);
Output:
Samir
This is useful when two modules export values with the same name.
We can import all named exports as one object.
math.js
export const add =
(a, b) => a + b;
export const subtract =
(a, b) => a - b;
export const multiply =
(a, b) => a * b;
Import:
import * as math
from "./math.js";
Usage:
console.log(
math.add(5, 5)
);
console.log(
math.subtract(10, 5)
);
console.log(
math.multiply(5, 5)
);
Output:
10
5
25
Think of:
math.add()
math.subtract()
math.multiply()
as accessing exports through a module namespace object.
Sometimes we want one file to collect exports from multiple files.
Suppose:
utils/
├── formatDate.js
├── formatCurrency.js
└── index.js
formatDate.js
export function formatDate(
date
) {
return date
.toLocaleDateString();
}
formatCurrency.js
export function formatCurrency(
amount
) {
return `Rs. ${amount}`;
}
index.js
export {
formatDate
} from "./formatDate.js";
export {
formatCurrency
} from "./formatCurrency.js";
Now:
import {
formatDate,
formatCurrency
} from "./utils/index.js";
This can make imports easier to organize.
Normal imports are usually loaded as part of the module dependency graph.
Example:
import { add }
from "./math.js";
Sometimes we only want to load a module when it is actually needed.
JavaScript provides:
import()
This is called a dynamic import.
Example:
const module =
await import(
"./math.js"
);
Usage:
console.log(
module.add(10, 20)
);
Output:
30
Suppose a user clicks a button before we need a module.
const button =
document.querySelector(
"#calculate"
);
button.addEventListener(
"click",
async () => {
const math =
await import(
"./math.js"
);
console.log(
math.add(10, 20)
);
}
);
The module can be loaded when that functionality is needed.
Dynamic imports can help with:
Code Splitting
Lazy Loading
Reducing Initial JavaScript
Loading Features When Needed
Conceptually:
Application Starts
↓
Load Required Code
↓
User Opens Feature
↓
Load Additional Module
Modern bundlers and frameworks can use this pattern for performance optimization.
| Feature | CommonJS | ES Modules |
|---|---|---|
| Name | CJS | ESM |
| Import | require() |
import |
| Export | module.exports |
export |
| Traditional Use | Node.js | Modern JavaScript |
| Browser Native Support | No | Yes |
| Static Imports | No | Yes |
| Tree Shaking Friendly | Limited | Yes |
| Modern Standard | No | Yes |
CommonJS:
const math =
require("./math");
ES Modules:
import * as math
from "./math.js";
CommonJS export:
module.exports = {
add,
subtract
};
ES Module export:
export {
add,
subtract
};
Browsers support ES Modules.
Suppose we have:
index.html
app.js
math.js
math.js
export function add(a, b) {
return a + b;
}
app.js
import { add }
from "./math.js";
console.log(
add(10, 20)
);
HTML:
<script
type="module"
src="app.js">
</script>
The important part is:
type="module"
type="module"?Without:
type="module"
the browser treats the script as a traditional script.
With:
<script
type="module"
src="app.js">
</script>
the browser knows the file uses the JavaScript module system.
This enables:
import
and:
export
Modules have their own scope.
Suppose:
user.js
const name = "Samir";
This does not automatically become a global variable available everywhere.
To use it somewhere else:
export const name =
"Samir";
Then:
import { name }
from "./user.js";
This helps prevent unnecessary global variables.
Modern Node.js supports ES Modules.
One common way to configure a project for ESM is using package.json:
{
"type": "module"
}
Then JavaScript files can use:
import something
from "./something.js";
and:
export default something;
Example with a package:
import express
from "express";
Projects using CommonJS may use:
const express =
require("express");
and:
module.exports =
something;
Therefore, when working with Node.js projects, you may encounter both:
CommonJS
and
ES Modules
Always check how the project is configured before mixing module syntax.
In JavaScript projects, you may encounter:
.js
.mjs
.cjs
.jsx
.ts
.tsx
Common meanings:
| Extension | Common Use |
|---|---|
.js |
JavaScript |
.mjs |
Explicit ES Module |
.cjs |
Explicit CommonJS |
.jsx |
JavaScript + JSX |
.ts |
TypeScript |
.tsx |
TypeScript + JSX |
Exact behavior can also depend on the runtime and project configuration.
When importing your own files, you commonly use relative paths.
Same directory:
import { add }
from "./math.js";
Parent directory:
import { add }
from "../math.js";
Nested directory:
import { getUsers }
from "./api/users.js";
Important:
./
↓
Current Directory
../
↓
Parent Directory
Modules can also come from installed packages.
For example:
import React
from "react";
or:
import express
from "express";
Notice:
"./math.js"
is a relative module path.
While:
"react"
is a package specifier resolved by the project's tooling/runtime.
A larger application might look like:
src/
│
├── api/
│ ├── userApi.js
│ ├── productApi.js
│ └── orderApi.js
│
├── components/
│ ├── Button.js
│ ├── Card.js
│ └── Navbar.js
│
├── utils/
│ ├── formatDate.js
│ ├── formatCurrency.js
│ └── validation.js
│
├── services/
│ └── auth.js
│
└── app.js
Each file has a clear responsibility.
formatDate.js
export function formatDate(
date
) {
return date
.toLocaleDateString();
}
app.js
import {
formatDate
} from "./utils/formatDate.js";
const today =
new Date();
console.log(
formatDate(today)
);
Instead of writing API requests everywhere:
const response =
await fetch(
"/api/users"
);
we can create a module.
userApi.js
export async function getUsers() {
const response =
await fetch(
"/api/users"
);
if (!response.ok) {
throw new Error(
"Failed to fetch users"
);
}
return response.json();
}
Then:
app.js
import {
getUsers
} from "./api/userApi.js";
const users =
await getUsers();
console.log(users);
Now the API logic is reusable.
validation.js
export function isEmailValid(
email
) {
return email.includes("@");
}
export function isPasswordValid(
password
) {
return password.length >= 8;
}
Import:
import {
isEmailValid,
isPasswordValid
} from "./validation.js";
Usage:
console.log(
isEmailValid(
"samir@example.com"
)
);
Output:
true
React applications heavily use modules.
A React component usually exists inside its own file.
Button.jsx
export default function Button() {
return (
<button>
Click Me
</button>
);
}
Then another component imports it.
App.jsx
import Button
from "./Button";
function App() {
return (
<div>
<Button />
</div>
);
}
export default App;
Conceptually:
Button.jsx
↓
Export Button
↓
App.jsx
↓
Import Button
↓
Render Button
A React project might contain:
src/
│
├── components/
│ ├── Button.jsx
│ ├── Navbar.jsx
│ └── Card.jsx
│
├── pages/
│ ├── Home.jsx
│ └── About.jsx
│
├── hooks/
│ └── useUser.js
│
├── utils/
│ └── formatDate.js
│
└── App.jsx
Modules make this structure possible.
Next.js applications also rely heavily on modules.
Example:
import Header
from "@/components/Header";
import {
getProducts
} from "@/lib/products";
Different responsibilities can live in separate modules:
components/
lib/
utils/
services/
hooks/
actions/
This keeps large applications manageable.
When one module imports another, a dependency is created.
Example:
app.js
↓
imports
↓
user.js
If user.js imports api.js:
app.js
↓
user.js
↓
api.js
This forms a:
Module Dependency Graph
For example:
app.js
/ \
↓ ↓
user.js product.js
↓ ↓
api.js api.js
Build tools can analyze these relationships.
Modern build tools can sometimes remove unused exported code.
Suppose:
math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
But the application only imports:
import { add }
from "./math.js";
Build tools may be able to remove unused exports from the final production bundle.
This optimization is called:
Tree Shaking
Conceptually:
Module
↓
Analyze Imports
↓
Find Unused Code
↓
Remove When Safe
↓
Smaller Bundle
ES Modules are particularly suitable for static analysis because their import/export structure is statically analyzable.
import { add }
from "./math.js";
Usually declared at the top level of a module.
The dependency can be analyzed before execution.
const math =
await import(
"./math.js"
);
Loaded dynamically when the code reaches that point.
Comparison:
| Static Import | Dynamic Import |
|---|---|
import ... from |
import() |
| Part of static dependency graph | Loaded dynamically |
| Common for normal dependencies | Useful for conditional/lazy loading |
| Easy to statically analyze | Returns a Promise |
Suppose multiple files need the same function.
Without a module:
Page A
↓
Copy Function
Page B
↓
Copy Function
Page C
↓
Copy Function
Now the same logic exists three times.
With a module:
formatDate.js
↓
↓
┌────┼────┐
↓ ↓ ↓
A.js B.js C.js
One implementation can be reused everywhere.
A useful principle is:
One Module
↓
One Clear Responsibility
Instead of:
everything.js
containing:
Authentication
Products
Users
Payments
Formatting
Validation
prefer modules such as:
auth.js
productApi.js
userApi.js
paymentApi.js
formatDate.js
validation.js
This is called separation of concerns.
Prefer ES Modules in modern JavaScript projects.
import {
add
} from "./math.js";
Use named exports when a module exposes several related utilities:
export function formatDate() {}
export function formatCurrency() {}
export function formatNumber() {}
Use default exports when a module has one clear primary export when that matches your project's convention:
export default function Button() {
}
Keep modules focused:
Good
userApi.js
productApi.js
authApi.js
Instead of:
everything.js
Use descriptive names:
formatDate.js
validateEmail.js
userApi.js
authService.js
Avoid unnecessary dependencies between unrelated modules.
math.js
function add(a, b) {
return a + b;
}
Then:
import { add }
from "./math.js";
will not work because add was never exported.
Fix:
export function add(a, b) {
return a + b;
}
If:
export const add =
(a, b) => a + b;
use:
import { add }
from "./math.js";
Not:
import add
from "./math.js";
For a default export:
export default function add() {
}
use:
import add
from "./math.js";
Remember:
Named Export
↓
{ name }
Default Export
↓
name
In a plain browser HTML setup:
<script src="app.js"></script>
does not declare the script as an ES module.
Use:
<script
type="module"
src="app.js">
</script>
Project:
project/
│
├── index.html
├── app.js
├── math.js
└── user.js
math.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
user.js
const user = {
name: "Samir",
age: 21
};
export default user;
app.js
import {
add,
multiply
} from "./math.js";
import user
from "./user.js";
console.log(
user.name
);
console.log(
add(10, 20)
);
console.log(
multiply(5, 5)
);
Output:
Samir
30
25
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
JavaScript Modules
</title>
</head>
<body>
<h1>
JavaScript Modules
</h1>
<script
type="module"
src="app.js">
</script>
</body>
</html>
Export:
function add(a, b) {
return a + b;
}
module.exports = add;
Import:
const add =
require("./math");
Export:
export function add(a, b) {
return a + b;
}
Import:
import { add }
from "./math.js";
Visual:
CommonJS
↓
require()
module.exports
ES Modules
↓
import
export
| Concept | Purpose |
|---|---|
| Module | Separate reusable code |
| CommonJS | Traditional Node.js module system |
require() |
Import CommonJS module |
module.exports |
Export CommonJS values |
| ES Modules | Standard JavaScript module system |
export |
Export values |
import |
Import values |
| Named Export | Export multiple named values |
| Default Export | One default export |
as |
Rename an import/export |
import * |
Import module namespace |
import() |
Dynamic module loading |
type="module" |
Enable ES modules in browser scripts |
| Tree Shaking | Remove unused code when possible |
| Code Splitting | Split application code into chunks |
Modules allow large JavaScript applications to be divided into smaller and reusable pieces.
Large Application
↓
Split Into Files
↓
Modules
↓
Export
↓
Import
↓
Reuse
The two module systems you will commonly encounter are:
CommonJS
↓
require()
module.exports
and:
ES Modules
↓
import
export
Modern JavaScript development primarily uses ES Modules:
export const add =
(a, b) => a + b;
and:
import { add }
from "./math.js";
The most important difference to remember is:
Named Export
↓
export const add = ...
↓
import { add }
Default Export
↓
export default ...
↓
import name
Modules are fundamental to modern application architecture:
React Components
↓
Modules
Next.js Pages
↓
Modules
Utility Functions
↓
Modules
API Functions
↓
Modules
Hooks
↓
Modules
Services
↓
Modules
As applications grow:
Modules
↓
Better Organization
↓
Reusable Code
↓
Separation of Concerns
↓
Easier Maintenance
↓
Scalable Applications
Understanding import and export is essential because modern JavaScript projects are built from many small modules working together rather than one giant JavaScript file.