JavaScript Basics #7 Modules — import and export

6 min read

The final post in the Basics series. As you build with the tools we’ve covered, a single file grows long quickly. This post covers the tool for splitting code across files — ES Modules.

Why do we need modules? #

A small script fits in one file, but real projects:

  • A single file with 100 functions is hard to read
  • The same variable name colliding in multiple places
  • Hard to track where a function was defined

To solve that, JavaScript has per-file code separation with selective imports — modules.

Named Export — names as-is #

Start with the simplest form.

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

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

export const PI = 3.14159;

Putting export in front of a function/variable exposes it externally. The consumer:

src/main.js
import { add, subtract, PI } from './math.js';

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

The braces in import { ... } are key. You pick only the names you need from what the module exposes. Importing a name that doesn’t exist errors.

Export at the end #

Instead of export at every declaration, you can group them at the end.

grouped at the end
function add(a, b) {
  return a + b;
}

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

export { add, subtract };

Functionally the same. Pick one style as a team convention.

Renaming — as #

renaming
// at export
export { add as plus, subtract as minus };

// at import
import { add as sum } from './math.js';

Used when the library’s internal name and the public name differ, or when names from different modules collide.

Default Export — the module’s “primary” #

Only one per module. Think of it as saying “this is the module’s primary value.”

src/Logger.js
export default class Logger {
  log(message) {
    console.log(`[LOG] ${message}`);
  }
}
importing
import Logger from './Logger.js';
// no braces, name is up to you

const logger = new Logger();
logger.log('hello');

The name in import name is up to the caller. The same default can be Logger in one file and MyLogger in another.

Is default really needed? #

Because libraries like React use import React from 'react', default exports can look essential — but the modern convention is to prefer named exports over default. Reasons:

  1. Better autocomplete — IDE knows the names in the module more precisely
  2. Safer refactors — renaming propagates across files
  3. When callers pick names freely, code consistency suffers

Named exports work well for most code. Default exports are best suited to components (React) or single-instance library returns (e.g., the result of createLogger()).

Re-export — gather in one place #

When you want one entry point that re-exports many files.

src/utils/index.js
export { add, subtract } from './math.js';
export { capitalize, slugify } from './string.js';
export { default as Logger } from './Logger.js';
consumer
import { add, capitalize, Logger } from './utils';
// './utils/index.js' is picked up automatically

A pattern for exposing multiple files in a folder as a single module. Libraries commonly use this — the entry-point file of an npm package typically looks exactly like this.

Side-Effect Import — execute only #

When you don’t want a value but want the module to run.

execute only
import './styles.css';        // apply CSS
import './polyfills.js';      // set up globals

Fits CSS, polyfills, or modules that do something automatically (e.g., register on a global). In practice, CSS is the most common.

Dynamic Import — call at runtime #

Using import like a function lets you fetch a module asynchronously.

dynamic import
async function loadHeavy() {
  const module = await import('./heavy.js');
  module.run();
}

button.addEventListener('click', loadHeavy);

Returns a Promise. The module’s exports come in as object properties.

This pattern is the basis of code splitting — download the code only when the user uses that feature. Next.js’s dynamic() and React’s lazy() are wrappers around this.

* import — everything as a namespace #

import all
import * as math from './math.js';

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

Every export from the module is brought in as properties of math. Fits when you don’t know exactly what’s there, or when you want to use it as a namespace. Otherwise, picking specific names like { add, PI } is cleaner.

Modules in the browser #

Loading a module script in HTML requires the type="module" attribute.

index.html
<script type="module" src="./main.js"></script>

With type="module":

  • import/export works
  • Behaves like defer automatically (runs after DOM is built)
  • Variables are isolated to module scope, not global

Without it (<script src="..."> plain), it’s old mode and import won’t work. Tools like Vite handle this for you.

.js extension — required? #

This is one of the most confusing parts in the JavaScript ecosystem.

  • Standard ES Modules (browser, Node): extension requiredimport './math.js'
  • Bundler-built code (Vite, webpack, Next.js): extension usually omittableimport './math'

This series follows the modern flow and specifies the extension. It works with both Node and the browser standard. That said, you’ll often see code in React/Next.js without extensions.

CommonJS — Node’s old module system #

Node had its own module system before ESM was standardized.

CommonJS — old Node style
// export
function add(a, b) {
  return a + b;
}
module.exports = { add };

// import
const { add } = require('./math');

When you see require / module.exports, that’s CommonJS. Common in older packages and older material. New code is mostly ESM (import/export), but for compatibility, the two systems often coexist.

Modern tools (Vite, Next.js, Bun) handle both automatically — at the introductory stage, you don’t need to worry about this too much.

Wrap-up #

What we covered:

  • ESM (ES Modules) is JavaScript’s standard module system
  • export and import { ... } — named exports are the default
  • export default — one per module; the caller picks the name freely
  • Named exports beat default for autocomplete/refactoring
  • Re-export gathers multiple files into one entry point
  • Dynamic import for async loading (basis of code splitting)
  • <script type="module"> enables ESM in the browser
  • CommonJS (require/module.exports) is Node’s old system

Closing the Basics series #

Across 7 posts we covered:

  1. Getting started and setup — building your environment (#1)
  2. Variables and typeslet/const, 8 types, primitive vs reference (#2)
  3. Control flow — if/for/switch, for...of (#3)
  4. Functions — declaration/expression/arrow, parameters, hoisting (#4)
  5. Objects and arrays — map/filter/reduce, spread, destructuring (#5)
  6. Strings and template literals — methods, regex basics (#6)
  7. Modulesimport/export (this post)

With these in hand you can confidently write small tools and scripts in JavaScript. The next Intermediate series covers classes, async (Promise/async/await), iterators, optional chaining, and fetch — tools that level up your modern JavaScript.

X