JavaScript Fundamentals: ES Modules: import and export
ES Modules let you split your JavaScript code across multiple files. Instead of dumping everything into one massive file, you can create small, focused modules that do one thing well and export the pieces other files need. This makes your code easier to organize, test, and maintain. In this tutorial, you’ll learn how to export from a module and import it elsewhere.
Why Use Modules?
Before ES Modules arrived in 2015, JavaScript had no built-in way to share code between files. Developers relied on workarounds like including multiple script tags in HTML, which created global variable pollution and made dependency management messy.
ES Modules solve these problems by giving each file its own scope. Variables and functions you declare in a module don’t accidentally leak into other files. You explicitly choose what to expose through exports, and other files explicitly request what they need through imports.
This approach also enables better tooling. Bundlers like webpack can analyze your imports and exports to remove unused code (tree-shaking), and browsers can load modules asynchronously for better performance.
The export Statement
You can export values from a module in two ways: named exports and a default export. A module can have multiple named exports but only one default export.
Named Exports
Named exports let you expose specific values from your module. You can export declarations directly, or export names that were declared elsewhere in the file.
// math.js - Export declarations directly
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export class Calculator {
constructor(initialValue = 0) {
this.value = initialValue;
}
add(n) {
this.value += n;
return this.value;
}
}
You can also export a list of names declared elsewhere:
// utils.js
const formatDate = (date) => {
return date.toISOString().split('T')[0];
};
const capitalize = (str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
// Export both at once
export { formatDate, capitalize };
You can rename exports to avoid conflicts or create cleaner public APIs:
// Renaming on export
export { formatDate as formatIsoDate, capitalize as titleCase };
Default Export
Each module can have one default export. The default export doesn’t have a fixed name, so the importing file gets to choose what to call it.
// logger.js - Default export
const log = (message) => {
console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
};
export default log;
// You can also write it inline:
export default function(message) {
console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
}
The difference matters when importing. Named exports must be imported with their exact names (or aliased), but default exports can be renamed to anything.
The import Statement
Importing lets you bring exported values into another module. There are several import forms to match different export patterns.
Named Imports
Use curly braces to import specific named exports:
// main.js
import { add, subtract, PI } from './math.js';
console.log(add(5, 3)); // 8
console.log(subtract(10, 4)); // 6
console.log(PI); // 3.14159
You can rename imports to avoid conflicts:
import { add as sum, subtract as minus } from './math.js';
console.log(sum(2, 3)); // 5
console.log(minus(10, 3)); // 7
Default Import
Import a default export without curly braces:
import log from './logger.js';
log('Application started'); // [LOG] 2026-03-07T04:00:00.000Z: Application started
Since default exports don’t have a fixed name, you can call the import whatever you want:
import myLogger from './logger.js';
import whatever from './logger.js';
// Both work - you're naming it yourself
Namespace Import
Import everything as a single object:
import * as MathUtils from './math.js';
console.log(MathUtils.add(2, 3)); // 5
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.Calculator); // [class Calculator]
The namespace object contains all exports as properties. The default export is available as the default property:
import * as logger from './logger.js';
logger.default('Hello'); // Works - default export is on .default
Combining Imports
You can mix default and named imports in one statement:
import log, { formatDate, capitalize } from './utils.js';
log(capitalize(formatDate(new Date())));
Using Modules in the Browser
To use ES Modules in a browser, add type="module" to your script tag:
<!-- index.html -->
<script type="module" src="main.js"></script>
<!-- You can also write inline modules -->
<script type="module">
import { add } from './math.js';
console.log(add(2, 3)); // 5
</script>
When you use modules in the browser, you need to run them through a local web server. Opening the HTML file directly in the browser (file:// protocol) won’t work due to CORS restrictions. If you’re using VS Code, the Live Server extension handles this nicely.
# Using Python's built-in server
python -m http.server 8000
# Or Node's http-server
npx http-server -p 8000
File Extensions
You might see modules with .mjs extension instead of .js. The .mjs extension makes it clear which files are modules, and Node.js treats them as ES modules by default. Both work in browsers:
// This works
import { add } from './math.js';
// So does this
import { add } from './math.mjs';
For a beginner, sticking with .js is fine, but know that .mjs exists if you encounter it.
Common Patterns and Gotchas
Importing from Packages
When importing from npm packages, use the package name directly:
import { format } from 'date-fns';
import chalk from 'chalk';
console.log(format(new Date(), 'yyyy-MM-dd'));
console.log(chalk.green('Success!'));
Modules Are Singletons
A module is executed only once. Subsequent imports get a reference to the same exported values. This is useful for sharing state:
// counter.js
export let count = 0;
export function increment() {
count++;
}
// main.js
import { count, increment } from './counter.js';
import { count as count2, increment as inc2 } from './counter.js';
increment();
console.log(count); // 1
console.log(count2); // 1 - same value!
Live Bindings
Exported values are live bindings to the original. If the exporting module changes a value, importers see the change:
// state.js
export let value = 'initial';
setTimeout(() => {
value = 'changed';
}, 1000);
// main.js
import { value } from './state.js';
console.log(value); // "initial"
setTimeout(() => {
console.log(value); // "changed" - updated from the other module!
}, 2000);
Re-exporting
You can re-export values from other modules, which is useful for creating “barrel” files that aggregate exports:
// index.js - Barrel file
export { add, subtract } from './math.js';
export { formatDate, capitalize } from './utils.js';
export { default as log } from './logger.js';
// Now consumers can import from one file
import { add, formatDate, log } from './index.js';
Summary
ES Modules give JavaScript a standard way to organize code across files. You export what you want to share using export, and import it elsewhere with import. Named exports work well for multiple related values, while default exports are perfect for a module’s main functionality.
In the browser, use type="module" on your script tags and serve files through a local web server. Remember that modules are singletons with live bindings, which affects how you think about shared state.
These fundamentals prepare you for working with npm packages, modern frameworks like React and Vue, and build tools like webpack and Vite. Your next step is to try building a small project with multiple module files to solidify these concepts.