Modules (CommonJS & ESM)
Reviewed & published by Brayan K
A module in Node.js is a self-contained file of code whose values you can share with other files — using CommonJS (require / module.exports) or modern ES Modules (import / export).
Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll be able to split a program across several files, share functions between them with both module systems, pull in Node's built-in core modules, and recognise the small differences between named and default exports.
What You'll Learn in This Lesson
- What a module is and why splitting code into files helps
- CommonJS: share values with module.exports and require()
- ES Modules: share values with export and import
- How to enable ESM with "type": "module" or the .mjs extension
- How to use Node's built-in core modules like node:path
- The difference between named exports and a default export
1️⃣ CommonJS: module.exports and require()
CommonJS is Node's original module system, and you'll still see it in the vast majority of existing projects. The idea is simple: one file attaches values to module.exports, and another file reads them back with require(). Here's a math.js file that exports two functions.
// math.js — a CommonJS module that EXPORTS two functions.
// Everything you attach to module.exports becomes available
// to any file that require()s this one.
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
// Share these two functions with the outside world.
module.exports = { add, multiply };Now a second file, app.js, brings those functions in with require('./math'). The leading ./ is important: it tells Node "look for a local file", not an installed package.
// app.js — IMPORTS math.js with require().
// The './' means "a local file in THIS folder" (not an npm package).
// You can leave off the .js extension; Node adds it for you.
const math = require('./math');
console.log(math.add(2, 3)); // -> 5
console.log(math.multiply(4, 5)); // -> 20
// You can also pull out just the pieces you want with destructuring:
const { add } = require('./math');
console.log(add(10, 7)); // -> 172️⃣ ES Modules: export and import
ES Modules (ESM) is the official JavaScript standard, the same one browsers use. Instead of module.exports you write export, and instead of require() you write import. To switch a Node project to ESM you either add { "type": "module" } to your package.json or give files the .mjs extension.
// math.mjs (or math.js with "type": "module" in package.json)
// ES Modules use the 'export' keyword instead of module.exports.
// A named export — exported under its own name:
export function add(a, b) {
return a + b;
}
// You can have many named exports per file:
export function multiply(a, b) {
return a * b;
}
// A default export — the file's single "main" value:
export default function subtract(a, b) {
return a - b;
}Notice the two flavours of export above: named exports (add, multiply) and a single default export (subtract). When you import, the default comes with no braces and the named ones go inside { }. In ESM you must include the file extension on local imports.
// app.mjs — IMPORTS with the 'import' keyword.
// In ESM you MUST include the .mjs / .js extension on local files.
import subtract, { add, multiply } from './math.mjs';
console.log(add(2, 3)); // -> 5
console.log(multiply(4, 5)); // -> 20
console.log(subtract(9, 4)); // -> 5
// Tip: 'subtract' is the default export, so it has no braces.
// 'add' and 'multiply' are named exports, so they go in { }.
const total = add(1, 1);
console.log(`total = ${total}`); // -> total = 2Your turn. The two files below work once you fill in the two blanks marked ___. Follow the 👉 hints, then run it and compare with the expected output.
// 🎯 YOUR TURN — two files, replace each ___ then run it.
// ===== greet.js =====
function sayHello(name) {
return `Hello, ${name}!`;
}
// 1) Share sayHello so other files can use it:
module.___ = { sayHello }; // 👉 the property that exports values
// ===== app.js =====
// 2) Bring in greet.js (it's a LOCAL file in this folder):
const greet = ___('./greet'); // 👉 the CommonJS import function
console.log(greet.sayHello('Sam'));
// ✅ Expected output:
// Hello, Sam!3️⃣ Built-in Core Modules and node: Imports
Node ships with dozens of core modules — ready-made tools like path (file paths), os (operating-system info), fs (files), and http (web servers). You don't install them; they come with Node. The modern style writes the node: prefix (for example require('node:path')) so it's obvious you mean the built-in, not an npm package.
Whichever module system you use, it helps to keep the difference between named and default exports straight. Here's a side-by-side comparison.
// NAMED exports: many per file, imported by their exact name in { }.
// export function add() {} -> import { add } from './math.js'
//
// DEFAULT export: one per file, you pick any name when importing.
// export default function () {} -> import anyName from './math.js'
// You can mix them in a single import line:
import subtract, { add, multiply } from './math.mjs';
// ^default ^named ^named
// CommonJS has no 'default' keyword — module.exports = X is the
// closest equivalent, and require() simply returns whatever X is:
const everything = require('./math'); // -> { add, multiply }Mini-Challenge: A Temperature Module
No blanks this time — just a brief and an outline to keep you on track. Write the two files yourself, run them, and check your output against the example in the comments. Splitting one job across two files is exactly the habit that scales to real projects.
// 🎯 MINI-CHALLENGE: a tiny "temperature" module (CommonJS)
// ===== temp.js =====
// 1. Write a function cToF(c) that returns c * 9 / 5 + 32.
// 2. Export it with module.exports so other files can require it.
// ===== app.js =====
// 3. require('./temp') and call cToF(100), then console.log the result.
//
// ✅ Expected output:
// 212
// your code hereCommon Errors (and the fix)
- "Error: Cannot find module './math'" — The path is wrong, or you forgot the ./ for a local file. Use require('./math') for a file in the same folder; require('math') (no ./) makes Node look for an installed npm package instead.
- "SyntaxError: Cannot use import statement outside a module" — You used import in a file Node treats as CommonJS. Fix it by adding "type": "module" to package.json, or renaming the file to .mjs.
- Mixing require and import — You can't put a top-level import in a CommonJS file, or call require() directly in an ES Module. Pick one system per file; if you truly must mix, use createRequire or dynamic import().
- module.exports vs exports — Reassigning the shortcut, like exports = { add }, silently breaks the export. Either add to it (exports.add = add) or assign the full module.exports = { add }. When in doubt, always use module.exports.
- "Cannot find module" in ESM — ES Modules require the file extension on local imports. Write import x from './math.js', not './math' — the missing .js is the usual culprit.
Pro Tips
- 💡 One job per file: keep each module focused on a single responsibility. Small, well-named files are far easier to test and reuse than one giant index.js.
- 💡 Prefer the node: prefix for core modules (require('node:fs')). It documents intent and protects you from an npm package accidentally shadowing a built-in name.
- 💡 New project? Reach for ESM. Add "type": "module" to package.json so import/export match the rest of the modern JavaScript world.
📋 Quick Reference — Modules
| Task | Syntax |
|---|---|
| Export (CommonJS) | module.exports = x |
| Import (CommonJS) | const x = require('./x') |
| Export (ESM, default) | export default x |
| Export (ESM, named) | export { x } |
| Import (ESM) | import x from './x.js' |
| Core module | require('node:path') |
| Enable ESM | "type": "module" |
🎉 Lesson Complete!
- ✅ A module is a file whose values you share with other files
- ✅ CommonJS: module.exports shares, require('./x') imports
- ✅ ES Modules: export shares, import brings in
- ✅ Enable ESM with "type": "module" or the .mjs extension
- ✅ Core modules like node:path are built in — no install needed
- ✅ A file has many named exports but only one default export
Practice quiz
In CommonJS, how do you export values from a file?
- export default x
- module.exports = x
- exports x
- share x
Answer: module.exports = x. CommonJS assigns what you want to share to module.exports.
Which function imports a module in CommonJS?
- import()
- include()
- require()
- load()
Answer: require(). require('./file') brings in a local CommonJS module.
What does the './' prefix in require('./math') mean?
- A package from npm
- A core built-in module
- A global variable
- A local file in the same folder
Answer: A local file in the same folder. './' tells Node to look for a local file, not an npm package.
Which keyword declares a NAMED export in ES modules?
- export
- module
- share
- public
Answer: export. ESM uses the export keyword for named exports.
How many default exports can a single ES module have?
- Unlimited
- Exactly one
- Zero only
- Two
Answer: Exactly one. A file can have many named exports but only one default export.
When importing a default export, named imports go in braces. How is the default written?
- In square brackets
- Without braces, with any name you choose
- Always named 'default'
- In double quotes
Answer: Without braces, with any name you choose. The default export is imported without braces and you may name it anything.
Which prefix makes it explicit you mean a built-in core module?
- core:
- lib:
- std:
- node:
Answer: node:. The node: prefix (e.g. require('node:path')) marks a built-in module.
What does package.json need so .js files are treated as ES modules?
- "type": "module"
- "esm": true
- "module": "es"
- "format": "esm"
Answer: "type": "module". Setting "type": "module" enables ESM for .js files.
In ESM, what is required on local file imports that CommonJS lets you omit?
- The word import
- Quotes
- The file extension (.js / .mjs)
- A leading slash
Answer: The file extension (.js / .mjs). ESM requires the explicit file extension on local imports.
What does require('./math') return when math.js does module.exports = { add, multiply }?
- Only add
- undefined
- A Promise
- The object { add, multiply }
Answer: The object { add, multiply }. require returns exactly whatever was assigned to module.exports.
Continue this course
- Previous: Running Scripts & the REPL
- Next: npm & package.json — Manage dependencies and scripts with npm and a package.json file
- Quick reference: Node.js cheat sheet › Modules & npm
Frequently asked questions
What's the difference between CommonJS and ES Modules?
CommonJS (CJS) is Node's original module system: you share values with module.exports and pull them in with require(), and it loads modules synchronously. ES Modules (ESM) is the official JavaScript standard used in browsers and modern Node: you share values with export and pull them in with import, and it supports features like top-level await and static analysis. CommonJS is still everywhere in existing Node code, while new projects increasingly choose ESM. Both work in current versions of Node.
How do I turn on ES Modules in a Node project?
You have two options. Either add "type": "module" to your package.json, which makes every .js file in the project an ES Module, or give individual files the .mjs extension. With "type": "module" set, you use import/export everywhere; if you still need a CommonJS file in that project, name it with the .cjs extension. By default (no "type" field) Node treats .js files as CommonJS.
Can I mix require() and import in the same file?
Not freely. A CommonJS file uses require()/module.exports; an ES Module uses import/export — you can't put a top-level import statement in a CommonJS file, and you can't use require() directly in an ES Module without a small workaround (createRequire). The simplest rule is: pick one system per file. ES Modules can import CommonJS packages, but the reverse (a require of an ESM-only package) needs dynamic import().
What's the difference between named exports and a default export?
A file can have many named exports, each imported by its exact name inside curly braces: import { add } from './math.js'. A file can have at most one default export, imported with no braces and any name you like: import anything from './math.js'. Named exports are great when a module offers several functions; a default export suits a module that has one obvious main thing.