Kodokon kodokon.com

ES modules: import/export, structuring a project

Structure your projects with ES modules by leveraging their static analysis and avoiding toxic barrels and circular dependencies.

8 min · 3 questions

Open this lesson in Kodokon

ES modules are static: import and export statements are resolved when the file is parsed, before any code runs. That is what enables tree-shaking (removing code that is never imported) and catching naming mistakes at build time. Named exports or default export? Named exports win on almost every front: refactor-friendly renaming, reliable autocompletion, and no way to import the same thing under ten different names. Save the default for the case where the module exports one obvious thing.

JAVASCRIPT
export const TAX_RATE = 0.2;

export function applyTax(price) {
  return price * (1 + TAX_RATE);
}

export default class Cart {
  items = [];
  add(item) {
    this.items.push(item);
    return this;
  }
}
cart.js: named exports for the utilities, default for the central class.

On the consuming side, four forms: named import (with optional aliasing via as), default import, namespace import (* as), and dynamic import import(), which returns a promise of a namespace. The latter is the building block of code splitting: only load a heavy module (editor, chart, PDF) at the moment the user actually needs it.

JAVASCRIPT
import Cart, { applyTax, TAX_RATE } from "./cart.js";
import * as cartModule from "./cart.js";

console.log(applyTax(100), TAX_RATE); // 120 0.2
console.log(cartModule.TAX_RATE); // 0.2

const lazy = await import("./cart.js");
const cart = new lazy.default();
console.log(cart instanceof Cart); // true
Static forms and dynamic import of the same module.

For organization, prefer splitting by feature (features/cart/, features/auth/) rather than by technical type (controllers/, helpers/): code that changes together lives together, and removing a feature means deleting one folder. Expose each feature through a narrow entry point and treat any import that bypasses that entry point as a boundary violation.

Knowledge check

Make sure you remember the key points of this lesson.

  1. What makes tree-shaking possible with ES modules?
    • Bundlers execute the code to see what is used
    • Imports and exports are static and can be analyzed without running the code
    • ES modules automatically compress dead code
  2. What does the expression import("./module.js") return?
    • The module namespace, synchronously
    • A promise resolved with the module namespace
    • A function that loads the module when called
    • Only the module's default export
  3. What is the main risk of barrel files (an index that re-exports a whole folder)?
    • They make named exports impossible
    • They can load the entire folder for a single import and hinder tree-shaking
    • They prevent dynamic import of the folder