JavaScript Modules

A module is just a JavaScript file. Instead of putting all your code in one giant file, you split it into multiple files — each one focused on doing one specific thing. Then you share code between files using export and import.

Think of a kitchen with separate stations. One station for chopping vegetables, one for cooking, one for plating. Each station does its own job independently. If something goes wrong at one station, the others are not affected. Modules work the same way — separate, focused, and independent.

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.

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 userName = "Alice";
let userAge = 30;

function formatUser(name, age) {
  return `${name} (${age})`;
}

function fetchUser(id) { /* ... */ }
function saveUser(user) { /* ... */ }
function deleteUser(id) { /* ... */ }
function validateEmail(email) { /* ... */ }
function validatePassword(pwd) { /* ... */ }
// ...hundreds more lines

// 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.

Think of it like labelling items in a box. Each item has a name tag. When someone opens the box, they grab exactly the item they need by name.

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. Each file can only have one default export.

Think of it like a restaurant that is famous for one dish. When you go there, you know exactly what the star item is. The default export is that star item.

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("Bob")); // Hello, Bob!
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("Alice")); // Hello, Alice!

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("Alice")); // Hello, Alice!
Tip: Import only what you need. This keeps your code clear and helps bundlers like Webpack or Vite remove unused code (a feature 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 alice = new User("Alice");
console.log(alice.greet()); // Hi, I am Alice

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.53...
console.log(square(4));       // 16
console.log(triangle(6, 8));  // 24
Convention: Most developers use default exports for the main thing a module provides (a class, a component, a main function) and named exports for everything else.
  • A module is a JavaScript file that shares code using 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.

  • What is a JavaScript module?
  • What is the difference between named exports and default exports?
  • How many default exports can a file have?
  • How do you import a named export?
  • Can you rename an import? How?
  • What happens to variables inside a module — are they global?
  • What is the as keyword used for in imports?
  • Can a file have both named and default exports?
  • How do you use modules in a browser (what attribute do you need on the script tag)?
  • What is tree-shaking and how do modules help with it?
Fetch API
OOP & Classes