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.

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.

A kitchen has separate stations - one 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 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.

It's 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.

Picture a restaurant 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("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. 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 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
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.

My take: I lean toward named exports for almost everything - even when the module only exports one thing. Why? Because named exports force consistent naming across your codebase. When someone imports a default export, they can name it anything: import Whatever from "./utils". That freedom sounds nice, but in a team project it leads to the same module being called five different things in five different files. Named exports also play better with autocomplete and refactoring tools. The one exception: if you are exporting a single React component that matches the file name, default export is the established convention and fighting it is not worth it.

  • 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?