Node.js ES Modules — Complete Guide to import, export, and Modern JavaScript Modules
In this tutorial, you will learn about Node.js ES Modules. We cover key concepts, practical examples, and best practices to help you master this topic.
ES modules (ESM) are the official JavaScript module standard using import and export statements, providing static analysis, tree-shaking, and top-level await in Node.js.
What You'll Learn
By the end of this tutorial, you'll use ES modules in Node.js: named and default exports, static and dynamic imports, package.json configuration, and migrating from CommonJS.
Why ES Modules Matter
ESM is the future of JavaScript modules. It enables better tooling through static analysis, smaller bundles via tree-shaking, and aligns browser and server module syntax. Modern frameworks like Express 5 support ESM natively.
Real-World Use
A new Node.js API project uses ESM to import Express, top-level await for database connections, and tree-shakable imports from utility libraries.
ESM Learning Path
flowchart LR
A[CommonJS Modules] --> B[ES Modules]
B --> C[Core Modules]
C --> D[File System]
D --> E[Express.js]
A --> F{You Are Here}
style F fill:#f90,color:#fff
Enabling ES Modules
Two ways to enable ESM in a Node.js project:
{
"type": "module"
}
Or use the .mjs file extension for individual files. CommonJS files use .cjs.
Named Exports and Imports
Named exports allow multiple exports per module with specific names.
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const PI = 3.14159;
// app.js
import { add, subtract, PI } from "./math.js";
console.log(add(5, 3)); // 8
console.log(PI); // 3.14159
Renaming Imports
import { add as sum, subtract as diff } from "./math.js";
console.log(sum(5, 3)); // 8
Default Exports
Each module can have one default export.
// logger.js
export default function log(message) {
console.log(`[LOG]: ${message}`);
}
// app.js
import log from "./logger.js";
log("Server started"); // [LOG]: Server started
Default exports can be imported with any name. This is convenient but reduces discoverability.
Combining Named and Default
// utils.js
export default function formatDate(date) { /* ... */ }
export const VERSION = "2.0";
export const AUTHOR = "DodaTech";
// app.js
import formatDate, { VERSION, AUTHOR } from "./utils.js";
Static vs Dynamic Imports
Static imports are evaluated at parse time. Dynamic imports load modules on demand.
// Static — must be at top level
import fs from "node:fs";
// Dynamic — can be conditional
async function loadPlugin(name) {
const module = await import(`./plugins/${name}.js`);
return module.default;
}
const plugin = await loadPlugin("auth");
Dynamic imports return a promise and work anywhere, including conditionals and functions.
Top-Level Await
ESM supports await at the top level without wrapping in an async function.
// db.js
import sqlite from "better-sqlite3";
const db = await sqlite(":memory:");
export default db;
Modules that depend on this will wait for it to resolve. This simplifies initialization code.
Package.json Exports
Modern packages define subpath exports for better Encapsulation.
{
"exports": {
".": "./dist/index.js",
"./utils": "./dist/utils.js",
"./package.json": "./package.json"
}
}
Users import specific paths: import { format } from "my-package/utils".
Common Mistakes
1. Forgetting File Extensions
ESM requires full file paths with extensions: import "./utils.js", not import "./utils".
2. Mixing require and import in ESM Modules
require is not available in ESM by default. Use createRequire from node:module if needed.
3. Importing JSON in ESM
JSON imports require assert syntax: import data from "./config.json" with { type: "json" }.
4. Circular Dependencies with ESM
ESM handles circular dependencies better than CommonJS but still causes issues. Restructure to avoid them.
5. Forgetting type:module in package.json
Without it, Node.js treats .js files as CommonJS. import statements throw SyntaxError.
Practice Questions
1. What is the difference between export default and export named?
export default exports a single value per module, imported without braces. Named exports export specific values, imported with matching names in braces.
2. Can you use require() in an ES module?
No, by default. Use import { createRequire } from "node:module" to create a require function if needed.
3. Why do ESM imports need file extensions?
ESM follows the ES spec, which requires complete specifiers. Node.js resolves extensions only in CommonJS.
4. What is tree-shaking and how do ES modules enable it?
Tree-shaking removes unused exports during Bundling. ESM's static structure lets tools know exactly what's used vs CommonJS's dynamic nature.
5. Challenge: Convert a CommonJS module to ESM that exports multiple utility functions and a default export.
// utils.mjs
export function formatDate(date) {
return date.toISOString().split("T")[0];
}
export function parseCSV(text) {
return text.split("\n").map(line => line.split(","));
}
export default { formatDate, parseCSV };
// app.mjs
import utils, { formatDate } from "./utils.mjs";
console.log(formatDate(new Date()));
console.log(utils.parseCSV("a,b\n1,2"));
FAQ
Mini Project: ESM Utility Library
Create a small library using ES modules with both named and default exports.
// string-utils.js
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function truncate(str, maxLen) {
return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
}
export default { capitalize, truncate };
// app.js
import strUtils, { capitalize } from "./string-utils.js";
console.log(capitalize("hello world")); // Hello world
console.log(strUtils.truncate("Long text here", 5)); // Long ...
What's Next
Node.js Path Module Node.js File System Express.js
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro