Skip to content

ES Modules

ECMAScript Modules (ESM) are JavaScript’s built-in way to split your code into smaller, reusable files. Instead of writing everything in one large file, you create separate files for different features and connect them using import and export.

Why this matters:

  • Code reuse: Write a function once, import it wherever you need it.
  • Encapsulation: Variables and functions inside a module are private by default. Nothing leaks out unless you explicitly export it.
  • Maintainability: Smaller, focused files are easier to understand, test, and update.

To use ES modules in HTML, add type="module" to your script tag:

index.html
<script type="module" src="app.js"></script>

A few things to keep in mind:

  • Use a web server. Modules must be served over HTTP/HTTPS. Opening HTML files directly (file://) won’t work due to browser security restrictions.
  • Include file extensions. Always write './formatters.js', not './formatters'. The .js extension is required.
  • Use relative paths. Start with ./ or ../ to specify the path relative to the current file.
  • Modules load asynchronously. They behave like <script defer>, so they won’t block page rendering.

  • Any JavaScript file becomes a module as soon as it uses export.
  • There’s no special syntax to declare a file as a module. You just create a .js file, write your code, and export what you want to share.

To share code from a module, you need to export it. There are two ways to do this.

Use named exports when a module shares multiple items. Add the export keyword before each variable or function:

// formatters.js
export const capitalize = (text) => {
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
};
export const truncate = (text, maxLength) => {
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text;
};

Use a default export when a module has one main thing to share. Each module can only have one default export:

// validator.js
export default function isValidEmail(email) {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test(email);
}

You can use named and default exports in the same file:

// productUtils.js
export const TAX_RATE = 0.08; // named export
export const formatPrice = (price) => { // named export
return `$${price.toFixed(2)}`;
};
export default function calculateTotal(price, quantity) { // default export
const subtotal = price * quantity;
return subtotal + (subtotal * TAX_RATE);
}

Once code is exported, you can import it into other files. The import syntax depends on how it was exported.

Use curly braces {} with the exact names that were exported:

// app.js
import { capitalize, truncate } from './formatters.js';
console.log(capitalize('hello world')); // "Hello world"
console.log(truncate('This is a long text', 10)); // "This is a ..."

No curly braces needed. You can choose any name you like:

// app.js
import isValidEmail from './validator.js';
console.log(isValidEmail('user@example.com')); // true
console.log(isValidEmail('invalid-email')); // false

When a module has both export types, import the default first, then named exports in curly braces:

// app.js
import calculateTotal, { TAX_RATE, formatPrice } from './productUtils.js';

Use * as to group all exports from a module under one name:

// app.js
import * as formatters from './formatters.js';
console.log(formatters.capitalize('hello')); // "Hello"
console.log(formatters.truncate('Long text here', 5)); // "Long ..."