Skip to main content

Command Palette

Search for a command to run...

Understanding JavaScript Modules

Updated
β€’6 min read
Understanding JavaScript Modules
L
I am a web developer who enjoys creating modern, responsive, and user-friendly web applications. My main technologies include HTML, CSS, JavaScript, React.js, and Node.js, which I use to build clean and efficient web solutions. Currently, I am pursuing my B.Tech at National Institute of Technology Patna, where I continue to strengthen my programming and problem-solving skills. I enjoy turning ideas into practical applications and solving real-world challenges through well-structured code and thoughtful design. I am always interested in learning new technologies, working with other developers, and contributing to meaningful projects. My long-term objective is to develop strong expertise as a full-stack developer and build applications that provide real value to users. πŸš€ Interested in opportunities related to web development, frontend development, and full-stack engineering.

From tangled global scripts to clean, maintainable code β€” a complete guide to ES modules, exports, and imports.

1. Why Modules? The Problem First

Before modules existed, every JavaScript file you loaded in a browser shared one big global scope. Every variable, every function β€” all of them lived together in the same room. That causes a predictable mess.

app.js & helpers.js (the old way)

// helpers.js β€” loaded first via <script>
var MAX_RETRIES = 3;
function formatDate(d) { return d.toString(); }

// app.js β€” loaded second via <script>
var MAX_RETRIES = 10; // ❌ Silently overwrites helpers.js
formatDate = "oops";      // ❌ Anyone can mutate anything

// And the dreaded script-order dependency:
// <script src="app.js"></script>      ← crashes, formatDate not yet defined
// <script src="helpers.js"></script>

As your codebase grows, this becomes a nightmare. There's no way to tell what depends on what. Rename one function and something unrelated breaks. Add a new library that uses the same global name as yours and everything explodes at runtime.

The core problem:

Without encapsulation, code is impossible to reason about in isolation. Every file can read and modify every other file's variables.

Modules solve this by giving each file its own scope. Nothing leaks out unless you explicitly export it, and nothing leaks in unless you explicitly import it.

2. Exporting β€” Sharing What You Build

A module keeps everything private by default. To make something available to other files, you use the export keyword. There are two flavours: named exports and the default export.

Named Exports

You can export as many things as you want from a single file by placing export in front of declarations. The names you export become the "public API" of that module.

math.js

export function add(a, b) {
  return a + b;
}

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

export const PI = 3.14159;

// This is NOT exported β€” it stays private to math.js
function _internalHelper() { /* ... */ }

You can also group exports at the bottom of the file, which many teams prefer for readability:

math.js (alternative style)

function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
const PI = 3.14159;

// All exports listed in one place β€” easy to scan
export { add, subtract, PI };

Default Export

Each module can have at most one default export. This is meant for the primary thing a module provides β€” a class, a main function, a config object.

logger.js

function log(message, level = 'info') {
  const time = new Date().toLocaleTimeString();
  console.log(`[\({level.toUpperCase()}] \){time}: ${message}`);
}

export default log; // The "main thing" this file provides

Tip: Think of named exports as a toolbox (multiple tools), and default exports as the one main item in a package.

3. Importing β€” Using What Others Built

The import statement always goes at the top of a file. It declaratively lists exactly what your file needs β€” making dependencies obvious at a glance.

Importing Named Exports

Use curly braces to pick specific exports by their exact name.

app.js

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

console.log(add(10, 5));       // 15
console.log(subtract(10, 5));  // 5
console.log(PI);                // 3.14159

Importing the Default Export

No curly braces needed. You can name it anything β€” that's one of the key traits of default imports.

app.js

import log from './logger.js';          // No curly braces
import myLogger from './logger.js';     // This also works β€” any name

log('Server started');         // [INFO] 10:32:01: Server started
log('File not found', 'error'); // [ERROR] 10:32:01: File not found

Renaming & Namespace Imports

If two modules export the same name, use as to rename on import. You can also import everything at once into an object with * as.

app.js

// Rename to avoid a naming conflict
import { add as mathAdd } from './math.js';
import { add as arrayAdd } from './array-utils.js';

// Import everything into a namespace object
import * as MathUtils from './math.js';

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

Using Modules in the Browser

To enable ES modules in an HTML file, add type="module" to your script tag. This one attribute unlocks strict mode, top-level await, and import/export syntax.

index.html

<!-- Old way: no isolation, order-dependent -->
<script src="helpers.js"></script>
<script src="app.js"></script>

<!-- New way: type="module" enables ES module syntax -->
<script type="module" src="app.js"></script>

The diagram above shows a realistic dependency structure. utils.js is imported by multiple modules β€” that's perfectly valid and common. Each module only sees what it imports; auth.js cannot accidentally break api.js because they don't share a global scope.

4. Default vs Named Exports

This is one of the most common points of confusion. Here's the practical difference:

// ─── Exporting ───────────────────────────────────

// Named β€” export multiple things
export const API_URL = 'https://api.example.com';
export const TIMEOUT = 5000;

// Default β€” export the main thing
export default class UserService { /* ... */ }

// ─── Importing ───────────────────────────────────

// Named β€” must use curly braces and exact names
import { API_URL, TIMEOUT } from './config.js';

// Default β€” no curly braces, any name works
import UserService from './user-service.js';
import Users       from './user-service.js'; // Also valid

// Both in one line
import UserService, { API_URL } from './user-service.js';

Recommendation: Prefer named exports for most cases. They're more explicit, easier to refactor (your editor knows the exact name), and enable better dead-code elimination.

5. Benefits of Modular Code

Adopting modules isn't just a syntax upgrade β€” it changes how you think about building software. Here's what you gain:

Quick Reference

Syntax What it does
export function foo() {} Named function export
export const X = 42 Named constant export
export { a, b, c } Group multiple named exports
export default foo Default export (one per file)
import { a, b } from './f.js' Import named exports
import foo from './f.js' Import default export
import { a as myA } from './f.js' Import with rename
import * as ns from './f.js' Import everything as namespace
import foo, { a, b } from './f.js' Import default + named together
<script type="module"> Enable modules in the browser

happy coding...