Advanced20 min read

ES Modules

Learn to organize code with ES modules — import/export syntax, named and default exports, and module patterns.

Why Modules?

As applications grow, keeping all code in a single file becomes unmanageable. Modules let you split code into separate files, each with its own scope.

Benefits of modules:

  • Code organization — group related functionality together.
  • Avoiding global namespace pollution — module variables are scoped, not global.
  • Reusability — import the same module in multiple files.
  • Encapsulation — only export what is needed; keep internals private.
  • Dependency management — clearly declare what each file depends on.

Before ES modules, developers used various patterns to achieve modularity:

IIFEs (Immediately Invoked Function Expressions):

javascript
const myModule = (function() {
  const privateVar = 'hidden';
  return { publicMethod: () => privateVar };
})();

CommonJS (Node.js):

javascript
const fs = require('fs');
module.exports = { myFunction };

ES modules (ESM) are now the official standard for JavaScript modules, supported natively in browsers and Node.js.

Named Exports & Imports

Named exports let you export multiple values from a module. Each export has a specific name that must be used when importing.

Exporting — use the export keyword before a declaration:

javascript
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export class Calculator { /* ... */ }

Or export multiple values at once using an export list:

javascript
const PI = 3.14159;
function add(a, b) { return a + b; }
export { PI, add };

Importing — use curly braces with the exact export names:

javascript
import { PI, add } from './math.js';
console.log(add(2, 3)); // 5
console.log(PI);        // 3.14159

Renaming on import with as:

javascript
import { add as sum } from './math.js';
console.log(sum(2, 3)); // 5

Importing everything as a namespace object:

javascript
import * as math from './math.js';
console.log(math.PI);       // 3.14159
console.log(math.add(2, 3)); // 5

Default Exports

A default export is the "main" thing a module provides. Each module can have at most one default export.

Exporting a default value:

javascript
// greet.js
export default function greet(name) {
  return 'Hello, ' + name + '!';
}

Or with an expression:

javascript
// config.js
export default {
  theme: 'dark',
  language: 'en'
};

Importing a default export — no curly braces needed, and you can use any name:

javascript
import greet from './greet.js';
import myConfig from './config.js';

console.log(greet('World'));    // 'Hello, World!'
console.log(myConfig.theme);    // 'dark'

Combining default and named exports:

javascript
// utils.js
export default function main() { /* ... */ }
export function helper() { /* ... */ }
export const VERSION = '1.0';
javascript
import main, { helper, VERSION } from './utils.js';

Default exports are commonly used for the primary export of a file — a class, a component, or the main function.

Module Features

ES modules have several important features that distinguish them from regular scripts.

Strict mode by default — modules automatically run in strict mode. You do not need 'use strict';.

Module-level scope — variables declared in a module are scoped to that module, not added to the global object:

javascript
// In a module
const secret = 'hidden'; // Not accessible from other modules

Imports are hoisted — import statements are moved to the top during parsing, regardless of where you write them.

Static structure — imports and exports are analyzed at build time, not runtime. This enables tree-shaking (removing unused code) in bundlers.

Dynamic import — for cases where you need to load a module conditionally or on demand, use import() as a function. It returns a Promise:

javascript
const button = document.getElementById('load');
button.addEventListener('click', async () => {
  const module = await import('./heavy-feature.js');
  module.init();
});

Dynamic imports are useful for code splitting — loading parts of your application only when needed, improving initial load time.

In the browser, use <script type="module"> to load ES modules:

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

Module Syntax Reference

javascript
// ========== Named Exports ==========

// Inline export
export const name = 'Kodojo';
export function greet(who) { return `Hello, ${who}!`; }

// Export list
const a = 1;
const b = 2;
export { a, b };

// Renaming on export
export { a as alpha, b as beta };


// ========== Default Export ==========

export default class App {
  constructor() { this.name = 'Kodojo'; }
}


// ========== Imports ==========

// Named imports
import { name, greet } from './module.js';

// Renaming on import
import { name as appName } from './module.js';

// Namespace import
import * as mod from './module.js';

// Default import
import App from './module.js';

// Default + named
import App, { name, greet } from './module.js';

// Dynamic import (returns a Promise)
const mod2 = await import('./module.js');

How many default exports can a module have?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.