Structure your projects with ES modules by leveraging their static analysis and avoiding toxic barrels and circular dependencies.
Open this lesson in KodokonES 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.
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;
}
}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.
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); // trueFor 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.
import("./module.js") return?