JavaScript ES6 Modules Import Export Guide

A practical reference for JavaScript ES6 module syntax. Learn every import and export pattern, from basic named exports to dynamic imports, with clear examples you can use immediately.

7 min read

ES6 modules give JavaScript a native way to split code across files. Before modules, developers relied on global variables and script loading order. Today, every modern JavaScript project uses import and export to organize code into reusable, maintainable pieces.

This guide covers the complete syntax. Each pattern includes a short example you can copy and adapt. For deeper explanations of specific topics, follow the linked articles.

The Core Pattern

A module is just a file. Anything you want to share, you export.

Anything you need from another file, you import. That is the entire mental model:

javascriptjavascript
// math.js - exports what it shares
export function add(a, b) { return a + b; }
 
// app.js - imports what it needs
import { add } from "./math.js";
console.log(add(2, 3)); // 5

The script tag in HTML tells the browser this is a module. Modules are deferred by default, meaning they load after the HTML parses:

htmlhtml
<script type="module" src="app.js"></script>
ES module import and export flow

The browser fetches each imported module once and caches it. Multiple files importing the same module share a single instance.

Named Exports

Named exports let a module share multiple values. Each exported value has a name that importers use to reference it. You can export inline on the declaration or group exports at the bottom of the file.

Exporting inline keeps the export visible next to the declaration:

javascriptjavascript
export const API_URL = "https://api.example.com";
export function fetchUser(id) { /* ... */ }
export class UserService { /* ... */ }

Grouping exports at the bottom creates a clean summary of the module's public API in one convenient place at the end:

javascriptjavascript
function formatDate(date) { /* ... */ }
function formatCurrency(amount) { /* ... */ }
 
export { formatDate, formatCurrency };

Import named exports with curly braces around the name. The names must match exactly between the export and import statements:

javascriptjavascript
import { formatDate, formatCurrency } from "./utils.js";

For detailed coverage including aliases and barrel files, see named exports.

Default Exports

A default export is the one primary value a module shares. Modules can have at most one default export. Import it without curly braces and give it any local name:

javascriptjavascript
// greet.js
export default function greet(name) {
  return `Hello, ${name}!`;
}
 
// app.js
import greet from "./greet.js";
console.log(greet("Alice")); // Hello, Alice!

A module can combine a default export with named exports in the same file. The default comes first in the import statement:

javascriptjavascript
import greet, { formatDate } from "./utils.js";

For more patterns and tradeoffs, see default exports.

Renaming Imports and Exports

Use the as keyword to rename when names collide or when you want a clearer local name:

javascriptjavascript
// Rename on import
import { formatDate as format } from "./dates.js";
 
// Rename on export
export { fetchUserData as getUser };

This avoids naming collisions without changing either source module. It is especially useful when two libraries export functions with the same name.

Re-Exporting from Barrel Files

A barrel file collects exports from multiple modules and re-exports them from one place. Consumers import from a single clean path:

javascriptjavascript
// utils/index.js
export { login, logout } from "./auth.js";
export { formatDate } from "./dates.js";

Now any file imports everything from one location, keeping the import list clean and predictable across the entire project codebase:

javascriptjavascript
import { login, logout, formatDate } from "./utils/index.js";

Avoid the wildcard form export * from when you want precise control over what gets re-exported. Explicit re-exports work better with tree shaking and make the public API auditable.

Namespace Imports

When you need everything from a module, import it all under a single namespace object:

javascriptjavascript
import * as MathUtils from "./math.js";
 
console.log(MathUtils.add(2, 3));       // 5
console.log(MathUtils.subtract(10, 2)); // 8

The namespace object contains every named export as a property. Default exports are not included in the namespace.

Dynamic Imports

Static imports load at parse time. Dynamic imports load on demand using the import() function, which returns a Promise:

javascriptjavascript
// Load a module only when needed
const { heavyFunction } = await import("./heavy-module.js");
heavyFunction();

Dynamic imports enable code splitting and conditional loading. The bundler automatically extracts dynamically imported modules into separate chunks.

Module Execution Order

ES modules execute exactly once, even when imported multiple times. The first import triggers execution, and subsequent imports return the cached module object. This guarantees predictable initialization:

javascriptjavascript
// config.js
console.log("Config loaded"); // Runs once
export const API_URL = "https://api.example.com";
 
// a.js
import { API_URL } from "./config.js"; // Logs "Config loaded"
 
// b.js
import { API_URL } from "./config.js"; // No log - module is cached

Imports are hoisted to the top of the module scope, so they execute before any other code in the file. This means you can reference imported values anywhere in the module body.

Browser vs Node.js Usage

In the browser, use the type attribute on script tags:

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

In Node.js, either use the .mjs file extension or set type to module in package.json for full ES module support:

jsonjson
{
  "type": "module"
}

Both environments support the same import and export syntax, but a few setup details differ:

BrowserNode.js
How to enabletype="module" script tag.mjs extension or "type": "module"
Path specifiersFull paths with extensions requiredResolves paths like traditional require
Top-level awaitSupportedSupported (ES modules only)

For how this compares to the older module system, see CommonJS vs ES modules.

Rune AI

Rune AI

Key Insights

  • Use export to share values from a module and import to bring them into another file.
  • Named exports let you share multiple values; default exports share one primary value.
  • Dynamic import() loads modules on demand and returns a Promise.
  • Re-export with export from to create barrel files that consolidate multiple modules.
  • ES modules run in strict mode with their own scope, unlike traditional scripts.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between ES modules and regular scripts?

ES modules use import and export syntax, run in strict mode by default, have their own scope, and are deferred by default. Regular scripts use global scope, run immediately, and cannot use import or export without a bundler.

Can I use ES modules in Node.js?

Yes. Node.js has supported ES modules since version 12. Use .mjs file extension or set type: module in package.json. Most modern Node.js projects use ES modules as the default.

Do I need a bundler to use ES modules?

No. All modern browsers support ES modules natively with the script type=module attribute. Bundlers are useful for production optimization like tree shaking and code splitting, but they are not required for development.

Conclusion

ES6 modules are the standard way to organize JavaScript code. The import and export syntax is clean, declarative, and supported everywhere from browsers to Node.js. Master the patterns in this guide and you will write modular code that bundlers can optimize, tools can analyze, and other developers can understand. For deeper dives into specific patterns, explore the linked articles on named exports, default exports, and dynamic imports.