JavaScript Curriculum
Splitting Code into Files
mediumThe Nexus codebase has grown. Everything is in one file: utilities, API calls, UI logic, validation — 3,000 lines. Nobody can find anything. A new engineer joins and doesn't know where to start. Modules are the solution: one responsibility per file, explicit contracts for what each file shares, and a bundler that ships only what the user's browser actually needs.
Why modules exist
In the early days of JavaScript, all code shared a single global scope. Every variable, every function — all visible everywhere, all potentially colliding. A library called utils.js might overwrite a function you defined. A third-party script might corrupt your user variable.
Modules solve this: each file gets its own scope. Nothing is visible outside unless you explicitly export it. Nothing is usable inside unless you explicitly import it. The dependencies are visible, auditable, and under your control.
Compare the two module systems:
exporting
// math.js
export const add = (a, b) => a + b
export const subtract = (a, b) => a - b
export const PI = 3.14159
// Default export — one per file:
export default function multiply(a, b) {
return a * b
}importing
// app.js
import multiply, { add, subtract, PI } from './math.js'
// Rename on import:
import { add as sum } from './math.js'
// Import everything as namespace:
import * as math from './math.js'
math.add(2, 3)
// Dynamic import — async, lazy loads:
const { add } = await import('./math.js')Asynchronous — non-blocking, safe for browsers
Must be at top level — not inside if blocks or functions (except dynamic)
Static structure — bundlers can tree-shake unused exports away
Native in all modern browsers with type="module" on <script>
✓ The universal standard. Works in browsers natively, in Node.js 12+, in all bundlers. Static — enables tree-shaking.
CommonJS (require) predates the language standard and works synchronously — it was designed for Node.js servers where file reads are fast. ES Modules (import) are the official standard: static, asynchronous, and natively understood by browsers and all modern bundlers.
Named exports, default exports, and everything in between
A single file can export multiple things. The five patterns cover every real-world case:
Any number per file. Import by exact name (or rename).
utils/math.js — exporting
export const add = (a, b) => a + b export const mul = (a, b) => a * b export const PI = 3.14159
app.js — importing
import { add, mul, PI } from './utils/math.js'
add(2, 3) // 5
mul(4, 5) // 20
PI // 3.14159The key distinction:
Named exports — export by name, import by the same name (or renamed with as). Any number per file. The source of truth for a file's public API.
Default export — one per file, imported with any name the caller chooses. Best for the primary thing a file provides — a class, a component, a main function.
Tree-shaking — only shipping what you use
Modern bundlers (Webpack, Vite, esbuild, Rollup) analyse your import statements at build time. Because ES module imports are static — they can't change at runtime — the bundler can determine exactly which exports are actually used. Everything else is eliminated from the final bundle.
add
120B
✓ kept
subtract
115B
~ kept (no shaking)
multiply
130B
✓ kept
divide
118B
~ kept (no shaking)
PI
24B
✓ kept
GOLDEN
24B
~ kept (no shaking)
formatCurrency
340B
✓ kept
formatDate
290B
~ kept (no shaking)
Tree-shaking only works with ES modules. require() is dynamic — a bundler can't know at build time which exports will be used, so it must include everything. This is one of the primary reasons CommonJS is being replaced.
Module resolution — how imports are found
Re-exporting — building a public API
A common pattern: an index.js file that re-exports from multiple sub-modules, giving consumers a single clean import point:
Your challenge
Simulate a three-export module: a named formatter function, a named constant, and a default validator. The import statement should use both named and default import syntax together. The challenge tests that you can read and write the two export styles and combine them in a single import line.
Challenge
Create a 'module' called userUtils. Export a named function called formatUser(user) that returns the string user.name + ' (' + user.role + ')'. Export a named constant called MAX_XP with value 10000. Export a default function called isAdmin(user) that returns user.role === 'admin'. Then write the import statement that gets all three into scope and log the result of each.
Splitting Code into Files
mediumThe Nexus codebase has grown. Everything is in one file: utilities, API calls, UI logic, validation — 3,000 lines. Nobody can find anything. A new engineer joins and doesn't know where to start. Modules are the solution: one responsibility per file, explicit contracts for what each file shares, and a bundler that ships only what the user's browser actually needs.
Why modules exist
In the early days of JavaScript, all code shared a single global scope. Every variable, every function — all visible everywhere, all potentially colliding. A library called utils.js might overwrite a function you defined. A third-party script might corrupt your user variable.
Modules solve this: each file gets its own scope. Nothing is visible outside unless you explicitly export it. Nothing is usable inside unless you explicitly import it. The dependencies are visible, auditable, and under your control.
Compare the two module systems:
exporting
// math.js
export const add = (a, b) => a + b
export const subtract = (a, b) => a - b
export const PI = 3.14159
// Default export — one per file:
export default function multiply(a, b) {
return a * b
}importing
// app.js
import multiply, { add, subtract, PI } from './math.js'
// Rename on import:
import { add as sum } from './math.js'
// Import everything as namespace:
import * as math from './math.js'
math.add(2, 3)
// Dynamic import — async, lazy loads:
const { add } = await import('./math.js')Asynchronous — non-blocking, safe for browsers
Must be at top level — not inside if blocks or functions (except dynamic)
Static structure — bundlers can tree-shake unused exports away
Native in all modern browsers with type="module" on <script>
✓ The universal standard. Works in browsers natively, in Node.js 12+, in all bundlers. Static — enables tree-shaking.
CommonJS (require) predates the language standard and works synchronously — it was designed for Node.js servers where file reads are fast. ES Modules (import) are the official standard: static, asynchronous, and natively understood by browsers and all modern bundlers.
Named exports, default exports, and everything in between
A single file can export multiple things. The five patterns cover every real-world case:
Any number per file. Import by exact name (or rename).
utils/math.js — exporting
export const add = (a, b) => a + b export const mul = (a, b) => a * b export const PI = 3.14159
app.js — importing
import { add, mul, PI } from './utils/math.js'
add(2, 3) // 5
mul(4, 5) // 20
PI // 3.14159The key distinction:
Named exports — export by name, import by the same name (or renamed with as). Any number per file. The source of truth for a file's public API.
Default export — one per file, imported with any name the caller chooses. Best for the primary thing a file provides — a class, a component, a main function.
Tree-shaking — only shipping what you use
Modern bundlers (Webpack, Vite, esbuild, Rollup) analyse your import statements at build time. Because ES module imports are static — they can't change at runtime — the bundler can determine exactly which exports are actually used. Everything else is eliminated from the final bundle.
add
120B
✓ kept
subtract
115B
~ kept (no shaking)
multiply
130B
✓ kept
divide
118B
~ kept (no shaking)
PI
24B
✓ kept
GOLDEN
24B
~ kept (no shaking)
formatCurrency
340B
✓ kept
formatDate
290B
~ kept (no shaking)
Tree-shaking only works with ES modules. require() is dynamic — a bundler can't know at build time which exports will be used, so it must include everything. This is one of the primary reasons CommonJS is being replaced.
Module resolution — how imports are found
Re-exporting — building a public API
A common pattern: an index.js file that re-exports from multiple sub-modules, giving consumers a single clean import point:
Your challenge
Simulate a three-export module: a named formatter function, a named constant, and a default validator. The import statement should use both named and default import syntax together. The challenge tests that you can read and write the two export styles and combine them in a single import line.
Challenge
Create a 'module' called userUtils. Export a named function called formatUser(user) that returns the string user.name + ' (' + user.role + ')'. Export a named constant called MAX_XP with value 10000. Export a default function called isAdmin(user) that returns user.role === 'admin'. Then write the import statement that gets all three into scope and log the result of each.