JavaScript Modules

Why should you care about JavaScript Modules?

Modules help you split large code into small focused files, avoid naming clashes, and reuse logic cleanly. If you work on real projects or in teams, this is one of the most important habits to learn early.

JavaScript projects commonly use two module systems. ES modules (ESM) use import and export. ESM is the standard modern JavaScript module system and works in modern browsers and Node.js. CommonJS uses require() and module.exports; it remains important when working with existing Node.js code and projects that use it.

Compare ESM and CommonJS before choosing an import style.
ConcernES modulesCommonJS
Exportexport { add } or export default addmodule.exports = { add }
Importimport { add } from "./math.js"const { add } = require("./math")
Typical environmentBrowsers and modern Node.jsOlder Node.js code and some tooling
Loading modelStatic imports help tools analyze dependencies.require() is traditionally evaluated at runtime.

A bundler such as Vite, Webpack, Parcel, or Rollup follows module imports, combines or splits files, and prepares optimized assets for production. Modern bundlers can use the static structure of ES module imports and exports to identify unused code and remove it during production builds. This process is called tree-shaking.

javascript

// math.js
export function add(first, second) {
  return first + second;
}

// main.js
import { add } from "./math.js";
console.log(add(2, 3));

javascript

// math.js
function add(first, second) {
  return first + second;
}

module.exports = { add };

// main.js
const { add } = require("./math");
Practical rule: follow the format already used by your project. Do not mix ESM and CommonJS casually; configure the runtime or bundler deliberately.

A circular dependency happens when module A imports module B while module B imports module A. ESM supports live bindings, but one module may read an export before the other module has finished initializing it. CommonJS may expose a partially initialized export object.

javascript

// user.js
import { formatUser } from "./format.js";
export const user = formatUser("Sam");

// format.js
import { user } from "./user.js";
export function formatUser(name) {
  return `${name} (${user?.role ?? "guest"})`;
}

Prefer a third module containing shared constants or functions, or move the shared operation behind a function call so initialization order is simpler. Circular imports are not always wrong, but they are a warning sign when exports are needed immediately at module load time.

Debugging clue: errors such as “cannot access before initialization” or unexpected undefined exports can point to an import cycle.

A module is a JavaScript file used as a self-contained unit of code. When a file is loaded as an ES module, it has its own module scope, and code is shared with other modules only through explicit export and import statements. This supports better organisation and reusable code without placing every variable in the global scope.

Without modules, as your codebase grows, you end up with:

  • One massive file with thousands of lines
  • Variables that accidentally overwrite each other
  • Code that is nearly impossible to test or maintain

javascript

// math.js - a module that handles math functions
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// main.js - import and use those functions
import { add, multiply } from "./math.js";

console.log(add(2, 3));       // 5
console.log(multiply(4, 5));  // 20

The math.js file exports two functions. The main.js file imports and uses them. Each file only knows what it needs to know. Clean and simple.

In a browser, load the entry file with <script type="module" src="./main.js"></script>. Serve the page through a local web server during development; opening an HTML file directly with file:// can prevent the browser from loading module imports.

When you first start coding, it is easy to put everything in one file. That works fine for small projects. But as soon as your app grows, having everything in one place becomes a problem.

Modules solve several real problems:

  • Organisation: Each file has a clear purpose - one for user logic, one for API calls, one for utilities
  • Reusability: Write a function once, import it anywhere you need it
  • No naming conflicts: Variables in one module do not leak into another
  • Easier to test: Small, focused modules are much easier to test in isolation
  • Team-friendly: Different developers can work on different files without stepping on each other

javascript

// Without modules - everything in one file, messy
let appName = "TaskBoard";
let appVersion = 3;

function formatApp(name, version) {
  return `${name} (v${version})`;
}

function fetchConfig(id) { /* ... */ }
function saveConfig(config) { /* ... */ }
function deleteConfig(id) { /* ... */ }
function validateEmail(email) { /* ... */ }
function validatePassword(pwd) { /* ... */ }
// hundreds more functions fill this file

// With modules - clean and organised
// user.js     -> handles user data
// api.js      -> handles server requests
// validate.js -> handles form validation
// main.js     -> ties everything together
Note: Modules have their own scope. Variables inside a module are not automatically global. They are private unless you explicitly export them. This is one of the biggest benefits - no accidental variable collisions.

A named export lets you export multiple things from a file. You put the export keyword in front of each thing you want to share. The name must match exactly when you import it.

Named imports use curly braces, and the local name normally matches the exported name. You can rename an import with as when a different local name is clearer or avoids a conflict.

javascript

// utils.js
export const PI = 3.14159;

export function greet(name) {
  return `Hello, ${name}!`;
}

export function square(n) {
  return n * n;
}

You can also export everything at the bottom of the file as a group, which some developers prefer for readability:

javascript

// utils.js - exporting at the bottom
const PI = 3.14159;

function greet(name) {
  return `Hello, ${name}!`;
}

function square(n) {
  return n * n;
}

export { PI, greet, square };
Note: A module can have as many named exports as it needs. Named exports are great when a file provides multiple related utilities.

A default export is used when a module has one main thing to share. You use the export default keyword, and each module can have only one default export. A default import does not use curly braces, and the importing file can choose its local name.

javascript

// user.js
export default class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `Hi, I am ${this.name}`;
  }
}

You can also default-export a function or a plain value:

javascript

// greet.js - default export of a function
export default function greet(name) {
  return `Hello, ${name}!`;
}

// config.js - default export of an object
const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000
};
export default config;
Remember: Each file can only have one default export. But it can have as many named exports as needed alongside it.

To import named exports, use curly braces {} and the exact name of the thing you exported. The name must match - spelled and cased exactly the same way.

javascript

// utils.js
export const PI = 3.14159;
export function greet(name) { return `Hello, ${name}!`; }
export function square(n) { return n * n; }

// main.js
import { PI, greet, square } from "./utils.js";

console.log(PI);           // 3.14159
console.log(greet("World")); // Hello, World!
console.log(square(4));    // 16

You do not have to import everything. Only import what you need:

javascript

// Only import greet - skip PI and square
import { greet } from "./utils.js";
console.log(greet("Developer")); // Hello, Developer!

You can also rename an import using the as keyword. This is useful when two modules export something with the same name:

javascript

import { greet as sayHello } from "./utils.js";
console.log(sayHello("Developer")); // Hello, Developer!
Tip: Import only what you need to keep dependencies clear. During production builds, bundlers may use the static structure of ES modules to remove code that is not used; this is called tree-shaking.

To import a default export, you do not use curly braces. You just choose any name you want - it does not have to match the name used in the other file.

javascript

// user.js
export default class User {
  constructor(name) {
    this.name = name;
  }
  greet() {
    return `Hi, I am ${this.name}`;
  }
}

// main.js - you can name it anything
import User from "./user.js";
// or: import MyUser from "./user.js";
// or: import Person from "./user.js";

const admin = new User("Admin");
console.log(admin.greet()); // Hi, I am Admin

You can import both a default export and named exports from the same file in one statement:

javascript

// shapes.js
export default function Circle(r) {
  return Math.PI * r * r;
}
export function square(n) { return n * n; }
export function triangle(b, h) { return 0.5 * b * h; }

// main.js
import Circle, { square, triangle } from "./shapes.js";

console.log(Circle(5));       // 78.53981633974483
console.log(square(4));       // 16
console.log(triangle(6, 8));  // 24
Practical guideline: Choose the export style that matches your project's conventions. Consistency across a project is more important than treating one style as universally better.
  • A module is a JavaScript file used as an ES module, with code shared explicitly through export and import
  • Modules have their own scope - variables are private unless exported
  • Named exports: use export keyword, imported with curly braces {}
  • Default export: use export default, each file can only have one, imported without braces
  • Named imports must use the exact same name, or rename with as
  • Default imports can use any name you choose
  • You can combine default and named imports in one statement
  • To use modules in a browser, use <script type="module">

What's next? Now that you know how to split code into modules, let's move on to OOP and Classes in the next tutorial.

Reviewed by

SimplyJavaScript Editorial Team

Technical editors and JavaScript educators with hands-on experience building frontend projects, writing learning material, and reviewing tutorials for clarity, accuracy, and beginner-friendly guidance.