Course: JavaScript Intermediate
Lesson 9: JavaScript Modules
Level
Intermediate
Estimated Reading Time
10–15 minutes
Goal
By the end of this lesson, the learner will be able to:
- Use
exportandimportto share code between JavaScript files - Explain the difference between named exports and default exports
- Describe why modules improve code organization and maintainability
Frontend Development Context
Every modern frontend project — whether plain JavaScript with a bundler, React, Vue, or Svelte — is built with modules. Modules let you split your code into separate files where each file has a clear responsibility: one file for API calls, one for utility functions, one for rendering logic. Without modules, all your code would live in one large file, creating naming conflicts and making collaboration difficult.
When you use import React from 'react' or import { useState } from 'react', you are using the JavaScript module system. Understanding how named and default exports work makes it much easier to read framework code, write your own reusable utilities, and organize a growing project.
Explanation
JavaScript modules use two keywords: export and import. Any value, function, class, or object can be exported from a file, and any exported thing can be imported into another file.
Named exports use the export keyword before a declaration. A file can have multiple named exports. When importing named exports, you use curly braces and must use the same name as the export (or rename it with as).
Default exports use export default. A file can have only one default export. When importing a default export, you do not use curly braces, and you can choose any name for the imported value.
Named and default exports can coexist in the same file. Many libraries export a default value (the main thing the module provides) and named exports for additional utilities.
The module system also creates private scope by default. Variables and functions in a module file are not accessible from the outside unless explicitly exported. This eliminates the global scope pollution that was a major problem in older JavaScript.
Code Example
This example shows a module file with named and default exports, and a second file that imports them.
// --- math.js (the module file) ---
// Named exports
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export const PI = 3.14159;
// Default export — the main thing this module provides
export default function calculateCircleArea(radius) {
return PI * radius * radius;
}
// --- main.js (the consumer file) ---
// Import named exports using curly braces
import { add, multiply, PI } from "./math.js";
// Import the default export — any name works
import calculateCircleArea from "./math.js";
// Rename a named import using 'as'
import { multiply as times } from "./math.js";
console.log(add(3, 4)); // 7
console.log(multiply(3, 4)); // 12
console.log(PI); // 3.14159
console.log(calculateCircleArea(5)); // 78.53975
console.log(times(6, 7)); // 42
Code Explanation
math.js exports two named functions (add, multiply), one named constant (PI), and one default export (calculateCircleArea). The default export is the primary function this module provides — the most important thing it offers.
In main.js, named imports use curly braces: { add, multiply, PI }. The names inside the braces must exactly match the exported names. This is different from default imports, which have no name constraint.
import calculateCircleArea from "./math.js" imports the default export. The name calculateCircleArea is chosen by the importer — it could be any valid identifier. The lack of curly braces signals that this is a default import.
import { multiply as times } renames the named export during import. Inside main.js, the function is referred to as times. This is useful when an imported name would conflict with a local variable name.
Pattern Highlights
✅ Positive Pattern: Use named exports for utility functions, constants, and secondary values. Use a default export for the primary thing a module provides — a component, a class, or a main function.
⚠️ Neutral Note: Some teams prefer to use only named exports for everything, avoiding default exports entirely. This makes imports more explicit and easier to search for in a codebase. Either approach is valid.
❌ Negative Pattern: Do not export everything from a file just because it exists there. Export only what other parts of the app actually need. Private helper functions should stay unexported.
Common Mistakes
Mistake 1: Using curly braces when importing a default export
Trying to destructure a default export as if it were a named export.
import { calculateCircleArea } from "./math.js"; // undefined — wrong syntax
Default exports do not use curly braces. Write import calculateCircleArea from "./math.js" without the braces.
Mistake 2: Expecting to rename a default import with as inside braces
Attempting to import and rename a default export using named import syntax.
import { default as area } from "./math.js"; // technically valid but unusual
The idiomatic way to rename a default import is simply to use any name you want directly: import area from "./math.js".
Mistake 3: Forgetting file extensions or paths in import statements
Leaving out the relative path or extension when importing local files.
import { add } from "math"; // fails in browser — no module resolution like Node.js
In the browser without a bundler, you must use a relative path and include the file extension: import { add } from "./math.js". Bundlers like Vite handle extensions automatically, but bare paths still need to match module aliases configured in the bundler.
Quick Recap
- Named exports use
exportbefore a declaration and are imported with curly braces:import { name } from "./file.js" - Default exports use
export defaultand are imported without curly braces:import anything from "./file.js" - A file can have many named exports but only one default export
- Module files have private scope — unexported values are invisible to the outside