Skip to main content

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));
tip

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

User.js
// models/User.js
export class User {
constructor(name) { this.name = name; }
}
Product.js
// models/Product.js
export class Product {
constructor(title, price) { this.title = title; this.price = price; }
}
index.js
// models/index.js: re-exports from sub-modules
export { User } from "./User.js";
export { Product } from "./Product.js";
app.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
// module-a.js
export default class A {}
export const nameA = "A";
module-b.js
// module-b.js
export default class B {}
export const nameB = "B";
combined.js
// 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:

  1. Autocomplete and discoverability: When you type import { } from "./validators.js", your editor can suggest all available exports
  2. Consistent naming: Everyone who imports validateEmail uses the same name (unless they explicitly rename with as)
  3. Tree-shaking: Unused exports are reliably removed from bundles
  4. Refactoring safety: Renaming an export causes import errors everywhere it is used, making it easy to find all references
  5. 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:

  1. Cleaner import syntax: No curly braces needed
  2. Flexible naming: The importer chooses the name, which can be convenient
  3. One file, one concept: Encourages modules that focus on a single class or function
  4. 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 CaseRecommendation
Utility modules (many related functions)Named exports
Class-per-file (React components, services)Default export (+ named exports for utilities)
Constants and configurationNamed exports
Library entry pointsNamed exports (better tree-shaking)
Single primary functionDefault export is fine
Team has no preferenceDefault 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); }
tip

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
warning

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

ConceptSyntaxKey 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 exportexport default ...One per module; importer chooses the name
Named importimport { x } from "..."Must match the export name exactly
Default importimport X from "..."No curly braces; any local name
Rename importimport { x as y } from "..."Avoids name collisions
Rename exportexport { x as y }Changes the public name
Namespace importimport * as M from "..."All exports on one object
Re-exportexport { x } from "..."Forward without local import
Re-export allexport * from "..."Forwards all named (not default) exports
Side-effect importimport "./module.js"Runs the module, imports nothing
Barrel fileindex.js with re-exportsSingle entry point for a directory
Named vs. defaultPrefer named for utilities; default for single-concept modulesNamed 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.