JavaScript Basics #7 Modules — import and export
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.
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:
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.
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
#
// 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.”
export default class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
}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:
- Better autocomplete — IDE knows the names in the module more precisely
- Safer refactors — renaming propagates across files
- 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.
export { add, subtract } from './math.js';
export { capitalize, slugify } from './string.js';
export { default as Logger } from './Logger.js';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.
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.
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 * 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.
<script type="module" src="./main.js"></script>With type="module":
import/exportworks- Behaves like
deferautomatically (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 required —
import './math.js' - Bundler-built code (Vite, webpack, Next.js): extension usually omittable —
import './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.
// 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
exportandimport { ... }— named exports are the defaultexport 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:
- Getting started and setup — building your environment (#1)
- Variables and types —
let/const, 8 types, primitive vs reference (#2) - Control flow — if/for/switch,
for...of(#3) - Functions — declaration/expression/arrow, parameters, hoisting (#4)
- Objects and arrays — map/filter/reduce, spread, destructuring (#5)
- Strings and template literals — methods, regex basics (#6)
- Modules —
import/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.