Skip to main content

Command Palette

Search for a command to run...

JavaScript Modules: Import and Export Explained

Published
6 min read
JavaScript Modules: Import and Export Explained
S
Full-stack developer obsessed with performance, scalability, and clean systems. I use Arch btw.

Introduction

Picture a codebase from five years ago. One file. Maybe two. A couple of <script> tags in your HTML, a few functions at the top, and a pile of variables that you promised yourself you'd clean up later. It worked. Sort of.

Then the project grew. More features, more functions, more developers. That one file ballooned into 2,000 lines. Scroll to find anything. Step over someone else's variable that happens to share the name you chose. Break three things trying to fix one. Sound familiar?

This is the problem JavaScript modules were built to solve.


The Mess Before Modules

In the early days, JavaScript had no native way to split code across files cleanly. Everything loaded into one global scope. If you had a utils.js and an app.js, both loaded via <script> tags, every variable and function from both files lived in the same space. Name a function formatDate in two files? One of them would silently overwrite the other. You would not get an error. You would just get the wrong output at the worst possible time.

Developers worked around this using patterns like IIFEs (immediately invoked function expressions) to create private scopes, or namespace objects to avoid collisions. These helped, but they were workarounds, not solutions. The code was harder to read, harder to share, and nearly impossible to test in isolation.

Modules changed that. A module is just a file with its own scope. Variables inside it do not leak out unless you explicitly say so. You choose what to expose and what to keep private. You decide what to pull in from elsewhere.


Exporting: Making Things Available

When you write a module, nothing in it is accessible from the outside by default. You have to export it.

There are two ways to do that: named exports and a default export.

Named Exports

Named exports let you expose multiple things from a single file. You attach the export keyword to whatever you want to share.

// math.js
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

export const PI = 3.14159;

Each of those three things is now available to other files, but under their exact names.

You can also export them all at the bottom in one shot:

// math.js
function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b;
}

const PI = 3.14159;

export { add, subtract, PI };

Same result. Some people find it cleaner to keep the exports at the bottom, since you can see what a file exposes without reading through all the implementation details.

Default Exports

A default export is for when a file represents one main thing. A class, a component, a primary function.

// greet.js
export default function greet(name) {
    return `Hello, ${name}`;
}

Each file can have only one default export. That's intentional. If you're exporting more than one main thing, named exports are probably the better fit.


Importing: Pulling It In

Once something is exported, you can import it from another file using the import statement.

Importing Named Exports

// app.js
import { add, PI } from './math.js';

console.log(add(2, 3));    // 5
console.log(PI);           // 3.14159

The curly braces are not optional here. The name inside must match exactly what was exported. This is the module system enforcing the contract between files: you exported add, so you import add.

If the name conflicts with something already in your file, you can rename it on the way in:

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

Importing a Default Export

Default exports skip the curly braces, and you can name them whatever you want on the import side:

// app.js
import greet from './greet.js';
console.log(greet('Sara')); // Hello, Sara

Because there's only one default per file, there's no ambiguity about what you're getting. The name greet here is just a local label.

Mixing Default and Named

A file can have both. A React component file is a common example:

// Button.js
export default function Button({ label }) {
    return `<button>${label}</button>`;
}

export const BUTTON_SIZES = ['sm', 'md', 'lg'];

And you import them together:

import Button, { BUTTON_SIZES } from './Button.js';

Default vs Named: When to Use Which

Use a default export when a file has one clear purpose: a single component, a single class, a single utility function that the file is named after. The file and the export are the same thing.

Use named exports when a file is a collection: a set of helper functions, a set of constants, a utilities module. Multiple things live there, and callers should be specific about what they need.

A common mistake is defaulting to default exports everywhere just because it feels simpler. It starts to hurt when you're trying to search for where something is defined. Named imports make that easy because the name is consistent across every file that uses it. With default imports, every file can invent its own name.


Why Modular Code Is Worth the Setup

Breaking your code into modules does more than just prevent naming collisions.

Each module has a clear responsibility. When something breaks in formatDate, you know to look in dateUtils.js, not scroll through a 3,000-line file hoping the relevant code is somewhere in the middle.

Modules are testable in isolation. You can import a single function, write tests for it, and run them without loading the entire application.

Modules make collaboration easier. Two developers can work in different files without stepping on each other constantly. Merge conflicts shrink. Code reviews get smaller.

Unused code gets easier to find and delete. If nothing imports from a file, that file is dead weight. That's much harder to see when everything is tangled together.


Conclusion

Think of a module like a toolbox. You build it, you put tools inside, and you lock it. Nobody can reach in and grab something unless you've left it on the counter (exported it). And when another person needs a specific tool, they have to ask for it by name (import it). No rummaging around. No accidentally grabbing the wrong thing.

That discipline is what makes large codebases navigable. Not clever tricks. Just clear boundaries, consistently applied.

Once you get used to writing code this way, going back to the global-everything approach feels genuinely uncomfortable. Which is probably the point.