How to Use Export and Import in JavaScript Modules
The export and import statements are the two sides of JavaScript's module system. export defines what a module shares with the outside world, and import declares what a module needs from other modules. Together, they create explicit, traceable dependencies between files.
While the basic syntax is straightforward, there are many variations: named exports, default exports, renaming, namespace imports, re-exports, and barrel files. Each serves a different purpose and comes with trade-offs. This guide covers every variation with practical examples, explains when to use each pattern, and highlights the best practices that keep large codebases maintainable.
Named Exports
Named exports allow a module to expose multiple values, each identified by its name. Consumers must import them using the exact same name (or explicitly rename them).
Inline Named Exports
You can export declarations directly by placing export before them:
// math.js
export const PI = 3.14159265358979;
export const E = 2.71828182845904;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
magnitude() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
}
Each export keyword marks that specific declaration as part of the module's public interface. Everything else in the file remains private.
Export List (Bottom-of-File Pattern)
Alternatively, you can write all your code first and export everything at the end using an export list:
// string-utils.js
const DEFAULT_SEPARATOR = ", ";
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
function truncate(str, maxLength, suffix = "...") {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - suffix.length) + suffix;
}
function slugify(str) {
return str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "")
.replace(/[\s_]+/g, "-");
}
function joinWords(words) {
return words.join(DEFAULT_SEPARATOR);
}
// Export everything at once
export { capitalize, truncate, slugify, joinWords };
DEFAULT_SEPARATOR is not in the export list, so it remains private to the module. The export list is not an object literal despite the curly braces; it is special export syntax.
Mixing Both Styles
You can combine inline exports and export lists in the same file:
// config.js
export const API_URL = "https://api.example.com";
export const TIMEOUT = 5000;
function buildHeaders(token) {
return {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
};
}
function buildUrl(path, params) {
const url = new URL(path, API_URL);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
}
return url.toString();
}
export { buildHeaders, buildUrl };
Both styles produce identical results. The bottom-of-file pattern makes it easy to see at a glance exactly what a module exports. The inline pattern keeps the export close to the declaration. Choose whichever reads better for your situation.
Default Exports
A module can have one default export. Default exports are used when a module has a single primary value to share, such as a class, a component, or a main function.
Exporting a Default
// User.js
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
return `Hi, I'm ${this.name}`;
}
}
// logger.js
export default function log(message) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
// constants.js
export default {
API_URL: "https://api.example.com",
TIMEOUT: 5000,
MAX_RETRIES: 3
};
Default Export of an Expression
Unlike named exports, default exports can export expressions directly without a name:
// multiplier.js
export default function(a, b) {
return a * b;
}
// greeting.js
export default "Hello, World!";
// config.js
export default {
debug: false,
version: "1.0"
};
// array.js
export default [1, 2, 3, 4, 5];
Default Export with a Name (For Stack Traces)
When exporting a function or class as the default, giving it a name helps with debugging:
// LESS USEFUL: anonymous default export
export default function(data) {
// Stack traces will show "default" or "(anonymous)" for errors here
throw new Error("Something went wrong");
}
// MORE USEFUL: named default export
export default function processData(data) {
// Stack traces will show "processData" (much easier to debug)
throw new Error("Something went wrong");
}
Combining Default and Named Exports
A module can have both a default export and named exports:
// http-client.js
export default class HttpClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async get(path) {
const response = await fetch(`${this.baseUrl}${path}`);
return response.json();
}
async post(path, data) {
const response = await fetch(`${this.baseUrl}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
return response.json();
}
}
// Named exports alongside the default
export const HTTP_STATUS = {
OK: 200,
NOT_FOUND: 404,
SERVER_ERROR: 500
};
export function createClient(baseUrl) {
return new HttpClient(baseUrl);
}
What Default Export Really Is
Under the hood, export default creates a named export called "default". These two are equivalent:
// Style 1: export default
export default function greet() {
return "Hello";
}
// Style 2: equivalent using named export
function greet() {
return "Hello";
}
export { greet as default };
Understanding this helps explain some of the import syntax variations covered below.
Named Imports
Named imports pull specific exports from a module by their exact names:
// Importing from the math.js module defined earlier
import { add, multiply, PI } from "./math.js";
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
console.log(PI); // 3.14159265358979
Import Only What You Need
You do not have to import everything a module exports. Import only what you use:
// math.js exports: PI, E, add, multiply, Vector
// Only need add and PI
import { add, PI } from "./math.js";
// E, multiply, and Vector are not imported (but they still exist in the module)
This is important for tree-shaking: bundlers like Webpack, Rollup, and esbuild can eliminate unused exports from the final bundle, reducing file size. This only works with named exports.
The Curly Braces Are Not Destructuring
A common misconception is that import { add, multiply } is object destructuring. It is not. It is special import syntax that looks similar but has different semantics:
// This is NOT destructuring:
import { add, multiply } from "./math.js";
// Destructuring would look like this and means something different:
// const { add, multiply } = require("./math.js"); // CommonJS, not ES modules
// You cannot use destructuring features in import statements:
// import { add: myAdd } from "./math.js"; // SyntaxError! (wrong rename syntax)
// import { add = defaultFn } from "./math.js"; // SyntaxError! (no defaults)
// import { ...rest } from "./math.js"; // SyntaxError! (no spread)
Default Imports
Default imports do not use curly braces. You provide your own name for the imported value:
// The imported name does NOT have to match the exported name
import User from "./User.js";
import MyUser from "./User.js"; // Same thing, different local name
import WhateverName from "./User.js"; // Still the same default export
const alice = new User("Alice", "alice@example.com");
console.log(alice.greet()); // "Hi, I'm Alice"
Importing Default and Named Together
// http-client.js exports:
// default: HttpClient class
// named: HTTP_STATUS, createClient
// Import both default and named exports
import HttpClient, { HTTP_STATUS, createClient } from "./http-client.js";
const client = new HttpClient("https://api.example.com");
// or
const client2 = createClient("https://api.example.com");
console.log(HTTP_STATUS.OK); // 200
The default import comes first (no curly braces), followed by named imports in curly braces, all from the same module specifier.
Importing Only the Default
import log from "./logger.js";
log("Application started");
Importing Only Named Exports (Ignoring the Default)
import { HTTP_STATUS, createClient } from "./http-client.js";
// The default HttpClient export is not imported
Renaming: import as and export as
Renaming Imports
Use as to rename imports when names collide or when you want a more descriptive local name:
// Both modules export a "validate" function
import { validate as validateUser } from "./user-validator.js";
import { validate as validateProduct } from "./product-validator.js";
validateUser({ name: "Alice" });
validateProduct({ price: 29.99 });
// Rename for clarity
import { t as translate } from "./i18n.js";
import { db as database } from "./database.js";
import { fmt as formatCurrency } from "./currency.js";
console.log(translate("welcome.message"));
console.log(formatCurrency(99.99));
Renaming Exports
Modules can also rename their exports:
// internal-utils.js
function internalHelperFunction() {
return "I have an ugly internal name";
}
function anotherInternalThing(data) {
return data.toString();
}
// Export with cleaner public names
export {
internalHelperFunction as helper,
anotherInternalThing as serialize
};
// consumer.js
import { helper, serialize } from "./internal-utils.js";
// Uses the renamed public names
Renaming the Default Export on Import
Since default imports already let you choose any name, renaming is implicit. But you can also use the as syntax with default:
// These two are equivalent:
import MyComponent from "./Component.js";
import { default as MyComponent } from "./Component.js";
This second form is rarely used directly, but it becomes useful in re-exports and namespace imports.
Importing Everything: import * as
The namespace import import * grabs all exports from a module and puts them on a single object:
// Import everything from math.js
import * as math from "./math.js";
console.log(math.add(2, 3)); // 5
console.log(math.multiply(4, 5)); // 20
console.log(math.PI); // 3.14159265358979
console.log(math.E); // 2.71828182845904
const v = new math.Vector(3, 4);
console.log(v.magnitude()); // 5
Accessing the Default Export via Namespace
If the module has a default export, it appears as the default property:
import * as HttpModule from "./http-client.js";
const client = new HttpModule.default("https://api.example.com");
console.log(HttpModule.HTTP_STATUS.OK); // 200
When to Use Namespace Imports
Namespace imports are useful when:
- A module has many exports and you want to use several of them
- You want to make the origin of each function clear in the code
- You want to avoid name collisions without renaming each import individually
- You are working with utility modules where the module name provides meaningful context
import * as validators from "./validators.js";
import * as formatters from "./formatters.js";
// Clear where each function comes from
const isValid = validators.email("test@example.com");
const formatted = formatters.currency(99.99);
Namespace Imports and Tree-Shaking
Modern bundlers can tree-shake namespace imports just as effectively as named imports. This was not always the case, but current versions of Webpack, Rollup, and esbuild handle it correctly:
import * as math from "./math.js";
// If you only use math.add, the bundler can still eliminate
// math.multiply, math.PI, math.E, and math.Vector from the bundle
console.log(math.add(2, 3));
Do not avoid namespace imports for fear of bundle size. Modern bundlers handle them well. Choose the import style that makes your code most readable.
Re-Exporting: export from
Re-exporting lets a module forward exports from other modules without importing them locally. This is the foundation of the barrel file pattern.
Basic Re-Export
// models/User.js
export class User {
constructor(name) { this.name = name; }
}
// models/Product.js
export class Product {
constructor(title, price) { this.title = title; this.price = price; }
}
// models/index.js: re-exports from sub-modules
export { User } from "./User.js";
export { Product } from "./Product.js";
// app.js: import from the barrel file
import { User, Product } from "./models/index.js";
const alice = new User("Alice");
const laptop = new Product("Laptop", 999);
The models/index.js file does not use User or Product itself. It simply forwards them, creating a convenient single entry point.
Re-Export with Renaming
// Re-export with a different name
export { User as UserModel } from "./User.js";
export { Product as ProductModel } from "./Product.js";
Re-Export Everything
// Forward all named exports from a module
export * from "./math.js";
export * from "./string-utils.js";
Important: export * re-exports all named exports but not the default export. This is a deliberate design choice to avoid ambiguity when multiple modules have default exports:
// module-a.js
export default class A {}
export const nameA = "A";
// module-b.js
export default class B {}
export const nameB = "B";
// combined.js
export * from "./module-a.js"; // Re-exports nameA, but NOT default class A
export * from "./module-b.js"; // Re-exports nameB, but NOT default class B
// To re-export defaults, you must be explicit:
export { default as A } from "./module-a.js";
export { default as B } from "./module-b.js";
Re-Export the Default
// Re-export another module's default as a named export
export { default as User } from "./User.js";
// Re-export another module's default as YOUR default
export { default } from "./User.js";
Common Re-Export Patterns
// utils/index.js: comprehensive re-export file
// Re-export everything from sub-modules
export * from "./string-utils.js";
export * from "./number-utils.js";
export * from "./date-utils.js";
// Re-export specific items with renaming
export { format as formatDate } from "./date-utils.js";
export { format as formatNumber } from "./number-utils.js";
// Re-export defaults as named exports
export { default as Logger } from "./Logger.js";
export { default as EventBus } from "./EventBus.js";
Re-Exported Values Are Not Available Locally
A key detail: export { X } from "./module.js" does NOT make X available in the current file:
// index.js
export { helper } from "./utils.js";
// helper is NOT available in this file!
// console.log(helper); // ReferenceError: helper is not defined
// If you need to use it locally AND re-export it:
import { helper } from "./utils.js";
export { helper };
// Or on separate lines:
// import { helper } from "./utils.js";
// ... use helper locally ...
// export { helper };
Named vs. Default Exports: Best Practices and Trade-Offs
This is one of the most debated topics in the JavaScript community. Both approaches have genuine advantages and disadvantages.
The Case for Named Exports
// validators.js: named exports
export function validateEmail(email) { /* ... */ }
export function validatePassword(password) { /* ... */ }
export function validateAge(age) { /* ... */ }
Advantages:
- Autocomplete and discoverability: When you type
import { } from "./validators.js", your editor can suggest all available exports - Consistent naming: Everyone who imports
validateEmailuses the same name (unless they explicitly rename withas) - Tree-shaking: Unused exports are reliably removed from bundles
- Refactoring safety: Renaming an export causes import errors everywhere it is used, making it easy to find all references
- Multiple exports per file: Natural for utility modules with several related functions
// Editor autocomplete shows:
// import { validate| } from "./validators.js"
// → validateEmail
// → validatePassword
// → validateAge
The Case for Default Exports
// UserProfile.js: default export
export default class UserProfile {
// ...
}
Advantages:
- Cleaner import syntax: No curly braces needed
- Flexible naming: The importer chooses the name, which can be convenient
- One file, one concept: Encourages modules that focus on a single class or function
- Framework conventions: React, Vue, and Angular commonly use default exports for components
// Clean import syntax
import UserProfile from "./UserProfile.js";
import Button from "./components/Button.js";
The Dangers of Default Exports
// user-service.js
export default class UserService { /* ... */ }
// consumer-a.js
import UserService from "./user-service.js"; // Good name
// consumer-b.js
import UserSvc from "./user-service.js"; // Different name
// consumer-c.js
import US from "./user-service.js"; // Cryptic abbreviation
// consumer-d.js
import ApiClient from "./user-service.js"; // Completely misleading name!
// All four work: but the inconsistency makes searching the codebase harder
With named exports, everyone uses the same name unless they explicitly rename with as, making global search-and-replace and refactoring much easier.
Recommendation
| Use Case | Recommendation |
|---|---|
| Utility modules (many related functions) | Named exports |
| Class-per-file (React components, services) | Default export (+ named exports for utilities) |
| Constants and configuration | Named exports |
| Library entry points | Named exports (better tree-shaking) |
| Single primary function | Default export is fine |
| Team has no preference | Default to named exports |
// GOOD: Utility module with named exports
// string-utils.js
export function capitalize(str) { /* ... */ }
export function slugify(str) { /* ... */ }
export function truncate(str, len) { /* ... */ }
// GOOD: Component with default export
// Button.js
export default function Button({ label, onClick }) {
return `<button onclick="${onClick}">${label}</button>`;
}
// GOOD: Service with default + named utilities
// api.js
export default class ApiClient { /* ... */ }
export const BASE_URL = "https://api.example.com";
export function createClient(config) { return new ApiClient(config); }
Many style guides (notably the Google JavaScript Style Guide and several prominent open-source projects) recommend named exports over default exports in most situations. Named exports provide better IDE support, enforce consistent naming across the codebase, and make refactoring safer. Consider using default exports only when there is a clear convention (like React components) or when a module truly has a single primary export.
Barrel Files (Index Exports)
A barrel file (also called an index file) is a module that re-exports items from multiple other modules, creating a single, convenient entry point for a directory of related modules.
Basic Barrel File
src/
├── models/
│ ├── User.js
│ ├── Product.js
│ ├── Order.js
│ └── index.js ← barrel file
// models/User.js
export class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
// models/Product.js
export class Product {
constructor(title, price) {
this.title = title;
this.price = price;
}
}
// models/Order.js
export class Order {
constructor(user, products) {
this.user = user;
this.products = products;
this.createdAt = new Date();
}
get total() {
return this.products.reduce((sum, p) => sum + p.price, 0);
}
}
// models/index.js: the barrel file
export { User } from "./User.js";
export { Product } from "./Product.js";
export { Order } from "./Order.js";
// app.js: clean import from the barrel
import { User, Product, Order } from "./models/index.js";
// Some bundlers and environments allow:
// import { User, Product, Order } from "./models";
const alice = new User("Alice", "alice@example.com");
const laptop = new Product("Laptop", 999);
const phone = new Product("Phone", 699);
const order = new Order(alice, [laptop, phone]);
console.log(order.total); // 1698
Without the barrel file, you would need separate imports:
// Without barrel: verbose and cluttered
import { User } from "./models/User.js";
import { Product } from "./models/Product.js";
import { Order } from "./models/Order.js";
Real-World Barrel File Structure
src/
├── components/
│ ├── Button.js
│ ├── Input.js
│ ├── Modal.js
│ ├── Card.js
│ └── index.js
├── hooks/
│ ├── useAuth.js
│ ├── useFetch.js
│ ├── useLocalStorage.js
│ └── index.js
├── utils/
│ ├── string.js
│ ├── date.js
│ ├── validation.js
│ └── index.js
└── services/
├── api.js
├── auth.js
├── storage.js
└── index.js
// components/index.js
export { default as Button } from "./Button.js";
export { default as Input } from "./Input.js";
export { default as Modal } from "./Modal.js";
export { default as Card } from "./Card.js";
// hooks/index.js
export { useAuth } from "./useAuth.js";
export { useFetch } from "./useFetch.js";
export { useLocalStorage } from "./useLocalStorage.js";
// utils/index.js
export * from "./string.js";
export * from "./date.js";
export * from "./validation.js";
// app.js: clean, organized imports
import { Button, Modal, Card } from "./components/index.js";
import { useAuth, useFetch } from "./hooks/index.js";
import { capitalize, formatDate, validateEmail } from "./utils/index.js";
The Barrel File Pitfall: Bundle Size
Barrel files can negatively impact bundle size if tree-shaking does not work perfectly. When you import one item from a barrel, the bundler might include the entire barrel:
// You only need capitalize
import { capitalize } from "./utils/index.js";
// But the barrel imports and re-exports EVERYTHING from all util files
// A less sophisticated bundler might include all of string.js, date.js, and validation.js
Modern bundlers (Webpack 5, Rollup, esbuild, Vite) handle this well in most cases. But if you notice unexpectedly large bundles, try importing directly:
// Direct import: bypasses the barrel, guarantees minimal inclusion
import { capitalize } from "./utils/string.js";
Best Practices for Barrel Files
// GOOD: Barrel for a cohesive set of related exports
// models/index.js
export { User } from "./User.js";
export { Product } from "./Product.js";
export { Order } from "./Order.js";
// GOOD: Use export * for pure utility modules (all small functions)
// utils/index.js
export * from "./string.js";
export * from "./number.js";
// CAUTION: Don't create mega-barrels that combine unrelated modules
// BAD: src/index.js that re-exports everything in the entire project
// export * from "./models/index.js";
// export * from "./utils/index.js";
// export * from "./services/index.js";
// export * from "./components/index.js";
// This makes tree-shaking harder and obscures where things come from
// CAUTION: Watch for name collisions with export *
// If string.js and date.js both export a "format" function,
// export * from both will cause a conflict
When using export * from multiple modules, be careful about name collisions. If two source modules export the same name, the barrel file will throw a SyntaxError at parse time. Use explicit re-exports with renaming to resolve conflicts:
// PROBLEM: both modules export "format"
// export * from "./string.js"; // has export function format()
// export * from "./date.js"; // has export function format()
// SyntaxError: duplicate export 'format'
// SOLUTION: rename to avoid collision
export { format as formatString } from "./string.js";
export { format as formatDate } from "./date.js";
Import and Export Syntax Quick Reference
All Export Forms
// Named exports
export const value = 42;
export function myFunc() {}
export class MyClass {}
export { name1, name2 };
export { name1 as alias1, name2 as alias2 };
// Default export
export default expression;
export default function() {}
export default function myFunc() {}
export default class {}
export default class MyClass {}
export { name as default };
// Re-exports
export { name } from "./module.js";
export { name as alias } from "./module.js";
export { default } from "./module.js";
export { default as Name } from "./module.js";
export { name as default } from "./module.js";
export * from "./module.js";
export * as namespace from "./module.js";
All Import Forms
// Named imports
import { name } from "./module.js";
import { name as alias } from "./module.js";
import { name1, name2 } from "./module.js";
// Default import
import defaultExport from "./module.js";
// Default + named
import defaultExport, { name1, name2 } from "./module.js";
// Namespace import
import * as module from "./module.js";
// Default + namespace (rare)
import defaultExport, * as module from "./module.js";
// Side-effect import (runs the module but imports nothing)
import "./module.js";
Side-Effect Imports
Sometimes you need to run a module for its side effects without importing any values:
// polyfill.js
if (!Array.prototype.at) {
Array.prototype.at = function(index) {
if (index < 0) index = this.length + index;
return this[index];
};
}
// app.js: import for side effects only
import "./polyfill.js"; // Runs the polyfill code, imports nothing
const arr = [1, 2, 3];
console.log(arr.at(-1)); // 3 (polyfill is active)
Common use cases for side-effect imports: polyfills, CSS imports (in bundlers), global configuration, and registration of plugins or components.
Summary
| Concept | Syntax | Key Points |
|---|---|---|
| Named export (inline) | export const x = ... | Can have many per module |
| Named export (list) | export { x, y, z } | Groups exports at the end of the file |
| Default export | export default ... | One per module; importer chooses the name |
| Named import | import { x } from "..." | Must match the export name exactly |
| Default import | import X from "..." | No curly braces; any local name |
| Rename import | import { x as y } from "..." | Avoids name collisions |
| Rename export | export { x as y } | Changes the public name |
| Namespace import | import * as M from "..." | All exports on one object |
| Re-export | export { x } from "..." | Forward without local import |
| Re-export all | export * from "..." | Forwards all named (not default) exports |
| Side-effect import | import "./module.js" | Runs the module, imports nothing |
| Barrel file | index.js with re-exports | Single entry point for a directory |
| Named vs. default | Prefer named for utilities; default for single-concept modules | Named exports offer better IDE support and refactoring |
Understanding these import and export patterns gives you complete control over how your JavaScript code is organized, what each module exposes, and how dependencies flow through your application. The key principle is explicitness: every export is a conscious decision about your module's public API, and every import is a conscious declaration of a dependency.