Modern JavaScript Modules: Mastering import and export
- Named exports (export const X) require curly braces and support Tree Shaking better than default exports.
- Default exports (export default X) are best for primary classes or components; they don't require braces.
- Dynamic
import()is a function that returns a Promise—ideal for code splitting and reducing initial bundle size.
ES Modules (ESM) use export to expose functionality and import to consume it. Use curly braces for named exports: import { helper } from './api.js'. Use no braces for default exports: import Consumer from './service.js'. In browsers, use . In Node.js, set "type": "module" in your package.json to enable modern syntax.
Named Exports: The Foundation of Clean APIs
Named exports are the preferred way to export multiple values, utilities, or constants from a single file. They force explicit naming at the call site, which improves code discoverability and makes 'Find All References' much more reliable in modern IDEs.
// io/thecodeforge/utils/math.js // Exporting individual components as they are defined export const PI = 3.14159265359; export const multiply = (a, b) => a * b; // Functional logic with explicit naming export function calculateForgeLoad(cpu, ram) { return (cpu * 0.7) + (ram * 0.3); } // main.js - Consuming named exports import { calculateForgeLoad, PI as MathPI } from './io/thecodeforge/utils/math.js'; console.log(`Forge Threshold: ${calculateForgeLoad(80, 16)}`); console.log(`Constant: ${MathPI}`); // Namespace Import: Useful for large utility libraries import * as ForgeMath from './io/thecodeforge/utils/math.js'; console.log(ForgeMath.multiply(10, 5));
Constant: 3.14159265359
50
Default Exports: Primary Module Identity
A module can have exactly one default export. This is typically reserved for the 'main' thing a file represents—such as a Class or a React Component. Unlike named exports, you can rename a default import to anything you like without using the as keyword.
// io/thecodeforge/services/Logger.js export default class Logger { constructor(prefix = 'FORGE') { this.prefix = prefix; } log(message) { console.log(`[${this.prefix}] ${new Date().toISOString()}: ${message}`); } } // You can still export secondary constants alongside a default export const DEFAULT_LOG_LEVEL = 'INFO'; // app.js import ForgeLogger, { DEFAULT_LOG_LEVEL } from './io/thecodeforge/services/Logger.js'; const logger = new ForgeLogger(); logger.log(`System initialized at level: ${DEFAULT_LOG_LEVEL}`);
Dynamic import(): Optimizing Performance
Static imports are resolved at parse time, meaning the browser downloads them before executing a single line of code. Dynamic returns a Promise, allowing you to 'lazy load' modules only when they are needed—for example, when a user clicks a button or navigates to a specific route.import()
// heavy-analytics.js export function runHeavyAudit() { console.log("Running intensive data audit..."); } // dashboard.js const auditBtn = document.querySelector('#audit-trigger'); auditBtn.addEventListener('click', async () => { try { // The module is fetched over the network only on click const { runHeavyAudit } = await import('./heavy-analytics.js'); runHeavyAudit(); } catch (err) { console.error("Failed to load the audit module:", err); } });
The Great Divide: CommonJS vs. ES Modules
Node.js was built on CommonJS (require), which is synchronous. ES Modules are asynchronous and static. Understanding how they interact is vital for modern full-stack development.
// LEGACY: CommonJS (example.cjs) const path = require('path'); module.exports = { name: 'ForgeLegacy' }; // MODERN: ES Modules (example.js with type: module) import { fileURLToPath } from 'url'; import { dirname } from 'path'; // Note: __dirname and __filename do not exist in ESM! const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export const metaData = { directory: __dirname }; // Top-level await is ONLY available in ESM const response = await fetch('https://api.thecodeforge.io/status'); const status = await response.json(); export { status };
🎯 Key Takeaways
- Named exports (export const X) require curly braces and support Tree Shaking better than default exports.
- Default exports (export default X) are best for primary classes or components; they don't require braces.
- Dynamic
import()is a function that returns a Promise—ideal for code splitting and reducing initial bundle size. - ES Modules are static: imports must be at the top level and are hoisted. CommonJS
require()is dynamic and can be called inside loops. - In ESM, global variables like __dirname are replaced by import.meta.url patterns.
Interview Questions on This Topic
- QExplain 'Tree Shaking' and why ES Modules make it possible while CommonJS makes it difficult.
- QWhat is the 'Static Analysis' benefit of ESM over CJS?
- QHow do you simulate __dirname in a Node.js ES Module environment?
- QWhat happens if two modules have a circular dependency in ESM vs. CommonJS?
- QImplement a dynamic import loader that retries the network request 3 times if it fails.
- QCompare 'Default' and 'Named' exports in terms of refactorability in a large-scale codebase.
Frequently Asked Questions
When should I use a default export vs a named export?
Industry standard at TheCodeForge suggests using named exports for utilities and constants because they force consistent naming across the codebase. Use default exports only for the 'Single Responsibility' of a file, like a specific React component. Avoid 'mixing' both in one file as it makes the import syntax cumbersome for other developers.
Can I import a CommonJS module in an ES Module file?
Yes, in Node.js, you can use import defaultExport from './file.cjs'. Node will wrap the module.exports of the CJS file as the default export of the ESM import. However, you cannot use inside an ES Module file; you must stick to require()import.
Why do I get 'Uncaught SyntaxError: Cannot use import statement outside a module'?
This happens in the browser when you forget to add type='module' to your <script> tag. Without this attribute, the browser treats the file as a legacy script where 'import' is a reserved word but not a functional command.
Does 'import' copy the value or create a reference?
ES Modules provide 'live bindings.' If a module exports a variable and then changes its value, the importing module sees that change. This is a major difference from CommonJS, which exports a snapshot/copy of the value at the time of the require call.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.